Skip to main content
Use this page to decode the raw ledger entries behind Soran’s contracts. For a validated name or payment answer, call Universal Lookup instead. The tables list storage keys; the code blocks define their stored values. Each contract section also explains how to interpret those values and maintain their storage lifetime.

Deployment scope

Match the contract’s current Wasm hash to the deployment manifest before using this schema. An upgrade can change the implementation without changing its address. Registrar and Resolver are namespace-specific. Discover their addresses through Lookup; the Nova pair in the address reference serves only .nova. The native claim storage page expands the Registrar’s claim types and receipts. The historical schema applies only to older deployments, whose Registry records lack the required ownership epoch.

Ledger and XDR conventions

The tables use Rust-style types to describe the stored schema. These names are not extra wrappers in XDR. All application storage in these six contracts is instance or persistent storage. None uses temporary storage. Persistent application entries use a LedgerKey::ContractData containing the contract address, encoded DataKey, and persistent durability. Instance keys live inside the storage map of the contract’s single SCContractInstance; they are not separate persistent entries named Config. The instance ledger key uses SCV_LEDGER_KEY_CONTRACT_INSTANCE. Contract Wasm is a separate contract-code ledger entry. Stellar storage reference Names use canonical lowercase ASCII labels: 1–63 bytes from a-z, 0-9, and an internal -. Let H mean SHA-256 and zero32 mean 32 zero bytes:
label_hash and ns_node are different values. A printable hex hash is not the stored BytesN<32> value: decode its hex into 32 bytes first. Addresses, labels, memo values and profile data are public chain data; removal or retraction does not erase transaction history.

Registry

Stores who owns a namespace and which contracts serve it. Registry code supports governance-authorized upgrades on the current testnet.

Storage keys

Provenance and ownership epochs

Clean and Tainted are single-symbol vectors. There are no current RegistrarTainted or ResolverTainted storage keys: both statuses are fields of Record. Ordinary owner/pointer updates preserve them. Tainted marks provenance ineligible for historical permanence, including historical upgrade paths or post-hoc Registrar attestation; restoring an earlier Wasm hash does not clear it. Approved governed upgrades instead update NativeV1(node) hashes and counters without setting the taint flag. Clean therefore proves neither attestation nor absence of upgrades. Resolver attestation and the current resolver pointer must be distinguished. Record.owner_epoch receives the next global ownership sequence on allocation, reserved award and accepted namespace transfer. An absent sequence is an error, not zero; overflow fails. Epochs can skip numbers for one namespace. Returning to a former owner does not reactivate their previous claim configuration. owner_epoch, template_hashes and native_contracts are read methods, not storage keys. The reservation Merkle root is stored, but the complete label list and allocation witnesses are not. Namespace deployment salts and vanity-search candidates are not stored application records. Salt version 1 is a code-defined namespace-and-role binding rule, not a DataKey or storage-schema version.

Transfers and storage upkeep

Namespace transfer proposals expire after 604800 seconds. A keeper can call touch_node(ns_node) to extend the namespace record and existing Registrar, Resolver and ResolverLock entries. It does not extend Pending or the Registry instance; keep_alive() covers the instance and code. A raw missing owner entry does not authorize reallocation: the contract also checks related state.

Allocator

Use label_hash = H(label_bytes) in this section. Objections and upfront claim fees use separate accounting, even when they use the same asset.

Storage keys

Claim states

ClaimState and FeeOutcome use their named variants, not 0, 1, or other numeric state IDs. Objection fields are meaningful while state = Objected; do not interpret leftover numeric obj_* fields as an active objection after settlement. announced_at can be adjusted when a frozen window resumes, so it is not always the original submission timestamp. rejected_at = 0 means no recorded rejection timestamp in that field. There are no Expired or Stuck ClaimState variants: those exits store Withdrawn in the claim while recording Expired or Stuck in the fee receipt. Missing Config, SchemaVersion or required FeePolicy is an error, unlike an intentionally absent optional claim. time_remaining = 0 alone does not prove that a claim can execute.

Fee receipts and credits

Fee amounts and credits are signed 128-bit integers in the configured token’s smallest units, with nonnegative accounting invariants. Decimals are token metadata, not part of these records. The current fee is 50000000000 stroops, or 5,000 testnet XLM. Receipt outcomes have these refund entitlements: The equivalent refund basis points are 0, 10000, and 8000; those numbers describe policy arithmetic, not the stored enum encoding. refund_amount and treasury_amount describe entitlements, including any undelivered pull-credit. A settled receipt does not by itself prove that every transfer was delivered. A later claim can overwrite the label’s old Claim/FeeReceipt; use confirmed events for history.

Objections, timing and history

ObjCount is capped at 3 objection cycles per claim epoch. The configured live-time budget and per-claimant ParkedAge survive different transitions from the per-label cycle counter. Rejection locks and claimant-specific reopen proposals are separate from the label’s latest claim. Reopen tuples bind a proposal to its precise rejection timestamp; a historical reopen does not necessarily free the label. Config.window is bounded to 86400–3153600000 seconds. max_claim_age is at most 3153600000 and must exceed the window by at least 3600 seconds. Rejection cooldown, objection timeout and the minimum banked concession budget each use 2592000 seconds. Those are timing/budget rules, not TTLs or automatic deletion deadlines. Claim basis accounting requires sum(entry.len() + 8) <= 4096, including the per-element overhead; an objection basis is at most 4096 raw bytes.

Storage upkeep and compatibility

touch_claim(label) extends the existing Claim, legacy ReopenEta, FeeReceipt, ObjCount, ActiveObjection, latest-decision pointer and latest decision, plus the instance/code. It does not enumerate older decisions, claimant-keyed history or all payee credits. touch_decision(case_id) maintains a known decision; touch_reopen_for(label, claimant) maintains that claimant’s proposal and matching rejection lock; touch_fee_credit(address) maintains that fee credit. There is no general enumeration of every persistent key. Known keys can also be maintained through Stellar’s ledger-entry TTL operations. Schema 4 requires fee receipts for claims. Schema 3 had no claim-fee receipt requirement; schema 2 used a single u64 ReopenEta value rather than the two-element tuple; legacy schema 1 had no SchemaVersion entry and different Claim/Config layouts. The current deployment was created fresh. There is no general raw-storage migration method: an upgrade must preserve the existing schema or implement a separately reviewed migration. Changing a version getter or stored version number does not convert old values. The 8 September Allocator upgrades preserve schema 4 and all existing key/value encodings. The objection interface adds separately keyed state. New PendingUpgrade.eta values equal the proposal’s ledger timestamp, allowing immediate execution. Config.window still controls allocation timing; it no longer adds a code-upgrade wait. Earlier proposals retain their stored eta.

Objection identities and decisions

With ruling_version() == 1, each new objection receives a case ID scoped to its network and Allocator. The global ObjectionSequence is stored with the instance and does not reset when a claim is dismissed, withdrawn, replaced or reopened. Overflow rejects the new objection instead of reusing an ID.
The legacy form applies only to objections already standing when the interface was installed, using their complete stored Claim snapshot. New objections write their active ID atomically with the objection and bond collection. An archived active-ID entry must be restored; it does not become an absent legacy entry. ActiveObjection is removed when the claim leaves Objected. Governance rulings are retained as independent records:
claim preserves the original Objected snapshot, including the evidence and parties. reason is Some(Bytes) for resolve_objection_for and None for the older label-only ruling method. Later claims do not overwrite decisions. The latest pointer is a convenience lookup, not a complete history. Normal persistent TTL and restoration rules apply. See the ruling methods and reason limits.

Reopening proposal storage

The reopening interface uses the following keys in the current testnet Allocator. Schema 4 and the existing (eta, rejected_at) tuples remain unchanged. The ReopenProposal returned by reopen_proposal combines the stored identity with its timing tuple; it is not an additional stored struct. New IDs hash the XDR tuple (Symbol("soran_reopen_v1"), network_id, allocator_address, sequence). Earlier proposals without an ID remain readable through a deterministic compatibility identity derived from their namespace, claimant, timing and storage path. An archived ID requires restoration rather than being treated as a missing legacy value. Recreating a proposal receives a new identity even in the same ledger. Cancellation, execution and related cleanup remove the matching ID with its proposal. Proposal upkeep also extends its existing ID. Use the read method to obtain an ID before a bound action instead of constructing one from raw storage.

Registrar

Stores each issued name, its holder and its ownership policy.

Storage keys

Ownership and generations

Config.owner is the constructor-time owner; current namespace authority follows Registry.owner_of(namespace_node). treasury_set_by identifies who configured the treasury. After namespace ownership changes, a treasury set by a previous owner is not the current owner’s authorized reclaim destination. Name.address is a built-in G/C destination, not a complete native payment instruction. The namespace Resolver may supply an explicit destination and memo. expires_at = 0 means no ownership expiry; it does not make a reclaimable namespace permanent. With a finite expiry, the name expires when ledger time is greater than expires_at. Storage TTL is independent. Generation starts at 0 for first issuance and increases on reissue, accepted transfer and reclaim. A holder address update or renewal does not change it. Resolver entries from another generation stop being current even if still present in the ledger. A transfer proposal carries both from and generation; its acceptance deadline is 604800 seconds after proposal. Acceptance changes holder and built-in address to the recipient and increments generation.

Ownership policy

Policy terms are either 0 or 86400–3153600000 seconds. trade_fee_bps is 0–10000; it is a basis-point field, not proof that a marketplace feature is enabled. The governed testnet rejects permanent code locking. Historical deployments retain their one-way permanence rules.

Native claim state

These versioned keys coexist with the original name/configuration keys. Public claim and reserved-assignment usage is cumulative; configuration changes do not erase history. Request receipts preserve the exact historical operation and are not proof of current ownership. Grace stores a separate duration and activation cutoff, without changing Name or ClaimConfig. See stored types, defaults, TTL and receipt encoding for the complete definitions. Name writes extend the name entry and instance without buying shared-code rent. Transfer proposal writes extend the proposal entry. touch(label) extends the Name entry only; keep_alive() maintains the instance and buys bounded code retention. Neither is a blanket renewal of every PendingTransfer. Renewal changes the ownership clock; a TTL extension does not.

Subname storage

subname_version() == 1 adds separate keys; it does not append fields to the original Name record.
The child node is SHA256(parent_node || SHA256(child_label)). First creation uses child generation 0; recreation increments it. Removal retains the record with active = false, preserving generation history and the enumeration label. The stored active bit is insufficient for live resolution: the parent must remain active in the recorded parent generation, and policy must allow use. Resolver records use the child’s own generation. Children have no independent holder or expiry field. touch_subname(parent, label) maintains the existing child, parent name, parent count and instance. touch_subname_page(parent, page) maintains one existing label page, parent count, parent name and instance. Neither changes policy, authority or ownership clocks, and the page call does not maintain every child record. Policy changes emit (Symbol("subpol_v1")) with SubnamePolicy. Creation and removal emit (Symbol("sub_v1"), child_node, child_generation) with SubnameChange; action is create or remove. Events are historical observations, not proof of current child activity.

Resolver

Stores holder-controlled destinations and profile records. Records are bound to an ownership generation.

Storage keys

Config.authority is the Registrar. admin remains in the layout but is not the current upgrade-authority source; current authority follows the Registrar’s live owner lookup. Provenance is instance data and cannot expire independently of Config.

Network address storage

multichain_version() == 1 identifies the typed network interface. Policies contain up to 14 distinct supported coin types. Payloads are binary, not text strings: EVM addresses use 20 bytes, Bitcoin uses a supported scriptPubKey, Solana uses 32 bytes, and XRP/Tron retain their required network prefixes. Use the matching SDK codecs for exact encodings. Reads require an active name, matching generation and enabled coin type. Disabling a network hides its records without deleting them. Clearing removes the selected key and decreases the stored-key count; archival and ownership changes do not decrease it. A stale record is not a current destination. Policy changes emit (Symbol("chain_pol")) with Vec<u32>. Both setting and clearing emit (Symbol("chain_v1"), name_node, generation) with the u32 coin type. Read the current record and policy to distinguish them; the event is not a payment instruction. touch_chain_address(name, coin_type) maintains an existing record and invokes the Resolver’s bounded code/instance upkeep.

Payment storage

There is no DataKey::Payment entry. set_payment and set_muxed atomically write these three entries for the same name and generation:
  1. Addr(name_node) → AddrRec with the destination G/C address; for muxed payments this is the base G account.
  2. Text(name_node, Symbol("payment")) → TextRec with one complete versioned string below.
  3. PaymentConfigured(name_node, generation) → true.
The exact encodings are:
The comments and alignment spaces above are explanatory. For example, the stored ID record is exactly:
Only the first three | characters delimit fields; text memo content may itself contain |. Direct records use a 56-character G/C StrKey. IDs use canonical decimal digits from 0 through 18446744073709551615, without leading zeros. Text memos contain 1–28 UTF-8 bytes. Hash memos contain exactly 64 lowercase hex characters. The decoder bounds the entire record to 128 bytes. A muxed record stores the base account and the exact u64 routing ID on chain; it does not store an M string as a Soroban Address. The SDK reconstructs the full checksummed M StrKey and returns {address: "M…", memo: {type: "none"}}. This operation-level ID is not a transaction memo. Reading only Addr would lose it. The published payment encoder/decoder implements the public SDK representation.
For example, resolve_destination returns [Symbol("Muxed"), {account: Address(G), id: U64(420)}]. resolve_v2 wraps that result in its resolution metadata. Neither return value is the raw TextRec stored on the Resolver. An untouched generation can resolve its current effective address with no memo. Once configured, missing, empty, malformed or mismatched payment metadata must not be treated as a memo-free destination. Presence of the marker is significant; the implementation never removes these generation-specific markers. Older generations do not configure a new holder’s generation. Generic set_text cannot edit the reserved payment key; use an atomic payment write.

Profiles, reverse names and maintenance

Profile keys are symbols of 1–32 letters, digits or underscores. Values are public strings. Retraction writes an empty string; it does not delete the ledger entry or erase history. Standard profile readers treat empty values as unset. AddrRec and TextRec are current only when their generation matches the Registrar’s current active name. Reverse records contain a plaintext two- or three-label name, its node and generation. A raw reverse entry is only a declaration: verified reads also check the current generation and forward match. Legacy reverse records without the name string, or with a differently typed name, are not current RevRec values; the destination address must authorize a new valid declaration. The older two-field Resolver Config is likewise not the current three-field layout. This deployment was created fresh; do not assume an older Resolver can adopt it through a Wasm swap alone. put extends the modified persistent record and instance without buying shared-code rent. keep_alive() maintains the instance and buys bounded code retention. touch_node(node), touch_text(node, key) and touch_reverse(address) extend their existing entry plus instance/code. touch_payment(name) covers the current-generation marker, address, payment text and instance/code together; maintaining the address alone does not maintain its payment metadata. None of these changes ownership or payment content.

Primary

Stores a G/C address’s chosen display name across namespaces. Exact-M elections live in Lookup.

Storage keys

Verifying a stored election

All PrimaryRec fields are an election-time snapshot. primary_of checks the Registry’s current resolver pointer and that Resolver’s verified display name. The cached node is informational, not an independent proof. A stored declaration can remain present while the verified answer is absent after a transfer, destination change, reverse clear or resolver change. An unavailable dependent display-name read can also yield no verified primary; that does not prove no raw declaration exists. Universal Lookup’s primary_name independently rechecks the returned name against current forward resolution. Its reverse path also verifies the complete native destination and refuses a muxed destination as an account-identity proof. Do not replace these checks with a raw PrimaryRec or an isolated Resolver address record that exposes only a muxed destination’s base G account. set_primary extends the record and instance without buying shared-code rent. clear_primary removes the current entry, not its historical transactions. touch_primary(address) extends the entry if present and maintains the instance with bounded code retention. The governed Primary supports upgrade(expected_hash, new_hash) under Registry governance. The one-time migration interface verifies source elections before sealing.

Lookup

Stores the shared read configuration and exact-M display-name elections. It does not copy namespace payment records.

Storage keys

The instance keys above hold configuration and upgrades. The muxed identity keys below hold persistent elections. Lookup does not copy payment records or memo instructions. Resolution structs, namespace/name metadata and the internal Provenance mirror are ABI data, not additional storage entries. An absent optional anchor is not a failed read; decoding or RPC failures must be handled separately. The status and batch read extensions add no storage keys or migrations. NameStatus, RegisteredName, LookupIdentity, IdentityName and BatchNames are argument/return types only. Their ledger/time values come from the invocation. name_status_version(), batch_read_version(), batch_read_limit() and primary_batch_limit() are code-defined getters. See their ABI and exact decoded shapes. version() and destination_version() are code-defined getters; neither has a stored DataKey. ABI 2 selects complete Direct/Muxed destinations. A successful native Resolver payment-version 1 response uses its direct-payment interface; version 2 requires destination-version 2. Failed or unknown capability responses do not justify a legacy fallback. The old Address-returning methods cannot represent a muxed ID and refuse that result. All Lookup configuration and pending-upgrade data share the instance lifetime. keep_alive() extends instance/code. The current upgrade delay is 0: governance still proposes an exact Wasm hash, and execution checks that proposal. Code replacement does not automatically convert the stored Config or other instance values.

Muxed identity storage

These keys require muxed_identity_version() == 1. Confirm the interface and code hash against the current deployment.
Both keys include the classic G account and its full SCV_U64 ID. The namespace key uses the normal 32-byte namespace node. Their exact XDR shapes are:
The value is a five-field symbol-keyed map. version is SCV_U32; generation is SCV_U64; name is SCV_STRING; both contract pointers are SCV_ADDRESS. Malformed shapes, wrong field types or an unsupported record version fail with InvalidMuxedRecord. muxed_identity_version() is a code-defined getter, not a storage key. ID 0 is a real muxed key; it does not address a G-account record. ID 18446744073709551615 must remain an exact bigint or decimal string in tools. Keys with the same G but different IDs never alias. No entry duplicates or replaces the Resolver’s payment data. These records capture an election-time snapshot. Reads recheck the same Registrar, Resolver, generation and complete current M payment. A transferred or reissued name needs a fresh election even if its new payment uses the same M address. An M Primary additionally requires the same verified reverse snapshot in its name’s namespace. A raw stored value alone is not proof that the election is currently valid. Election and clear calls require the base G account’s authorization. They maintain the instance without renewing shared code. Clear removes only the exact key and leaves transaction history. touch_reverse_muxed(namespace,account,id) and touch_primary_muxed(account,id) are permissionless: each maintains one existing entry and Lookup’s instance, plus bounded code renewal as described below, without creating an entry or validating its proof. keep_alive() maintains the instance and performs bounded code renewal; it does not maintain every election. Archived keys require restoration; an unavailable read is not an empty election. Versioned semantics are identified by these event topics and data: The set events preserve record.version = 1; clear events preserve the exact key. These events describe elections and retractions, not payment updates or current validity. Touch calls emit no election event.

TTL and restoration

Ownership expiry uses Unix seconds in stored values. Storage TTL uses ledger numbers in Stellar’s TTL entries and RPC liveUntilLedgerSeq. An ownership term of zero is not an infinite storage TTL. Ordinary reads do not provide a persistent keeper service, and read simulations do not commit storage changes. Registry, Allocator, Registrar and Resolver use these dynamic extension parameters. Primary retains the same parameters for its records and instance:
Lookup uses threshold = min(2_500_000, max_ttl) and extend_to = min(3_000_000, max_ttl) for its records and instance. These are ledger targets, not a promised number of days. A threshold-based bump only extends an entry below its threshold; it can be a no-op for an already-long TTL. Code and instance TTLs are independent. The combined instance-extension operation checks both separately. Instance TTL semantics Registrar name writes, Resolver payment/profile writes and Lookup/Primary ordinary display-name writes use instance-only extension. They retain the existing record and instance targets without extending shared Wasm code. A newly uploaded replacement can therefore have a shorter code lifetime than the instance, without making the next display-name writer fund a long code renewal. Explicit maintenance handles code separately: Registrar keep_alive; Resolver keep_alive and its address/text/reverse/network-address touch paths; Lookup keep_alive, touch_reverse_muxed and touch_primary_muxed; and Primary touch_primary extend code by at most 17,280 additional ledgers per call, capped at the configured target and network maximum. Resolver touch_payment invokes the bounded code helper twice, through its address and payment-text touch calls, so it can add up to 34,560 code ledgers before the target cap. Repeated calls can accumulate extensions. The caller pays transaction costs and any storage rent; the ledger bound is not a fixed XLM price. Record and instance extension remains threshold-based and can cost more when those entries need renewal. Registry and Allocator retain their own contract extension behavior; these methods do not introduce sponsorship. Persistent entries and contract instances can archive and later be restored. Archival does not establish that a namespace is unowned, a name has expired, a rejection/credit never existed, or a payment needs no memo. A raw getLedgerEntries result that omits a key is not a sufficient payment or ownership decision. A transaction accessing archived persistent data must include the required restoration work or fail; RPC simulation can identify what is needed. Restoration costs and subsequent TTL extension are distinct from name-renewal or claim fees. Stellar state archival Use the relevant contract read after restoration to recheck current meaning. Restoring an old-generation payment or a stale primary declaration does not make it current. Do not translate an RPC failure, archived entry or malformed ScVal into None, false, zero credit, or a memo-free G address.

Hosted keeper coverage

The hosted API’s keeper maintains known records. It is not an enumeration of all on-chain storage and does not guarantee that every record stays live. M election candidates come from indexed events, including a bounded history scan. Historical claimant candidates preserve the exact (label, claimant) pair and include ParkedAge, RejectedLock and ClaimantReopenEta. touch_claim alone does not cover those claimant-specific keys. A missing candidate can remain outside keeper coverage. An absent optional entry may never have existed or may have been cleared. Archived entries need inspection and restoration; a touch cannot restore them. Restoration preserves stored values and does not make a stale election valid or clear a rejection. For independent upkeep, use the per-contract methods described above or Stellar’s ledger-entry TTL operations. Check service status for the hosted service’s reported state.

Read raw keys with Stellar RPC

This Node example uses @stellar/stellar-sdk@17.0.1 and @sorandomains/lookup@0.11.0. It reads a Registry namespace record and the Resolver’s raw address, payment text and generation marker. It only reads state. Use an active name from the live examples, or one you have issued.
The helper key("Text", …, symbol("payment")) uses a symbol for the reserved key. Replacing it with a string, using the namespace hash instead of the name node, or querying the marker with a previous generation addresses a different ledger key. scValToNative is a decoder, not a schema validator or payment verifier. Keep the TextRec.value and matching AddrRec/generation together when inspecting them, and use resolvePayment for the validated payment answer. To inspect instance data instead, fetch its special footprint and decode each internal key/value pair:
The second snippet continues the first snippet’s server and deployment variables. It reads Config inside instance storage; querying persistent(registry, key("Config")) would look for a different, unused application entry. RPC returns current ledger data and TTL metadata, not a complete historical archive. getLedgerEntries reference

Governed testnet upgrade storage

Existing name and payment keys keep their encodings. Governed upgrades add separate keys:
Use native_code(node) for an existing namespace’s expected executables. template_hashes() returns defaults for new factories. Primary’s upgrade authority follows its configured Registry. Approved code changes retain addresses; replacing a contract creates separate storage. The migration storage reference documents the additive lineage, import markers and election records across all six contracts.

Bulk identity read helpers

The optimized Lookup uses these read-only helpers internally. They add no storage keys. Callers should use Lookup’s reverse_names and primary_names for verified display names: the raw snapshots below are not independently verified identity answers. Each destination request is [nodeBytes, generationBigInt, builtinAddress]. Its result uses the existing destination enum. The supplied generation and fallback address are inputs, so the result alone is not proof of a live record. Lookup obtains them independently from the current Registrar and checks each election against current namespace routing. explicitForwardBoolean identifies an explicit forward address at that generation; G/C reverse verification requires it. These optional tuple results encode absence as Soroban Void, decoded to JavaScript null. Lookup’s public IdentityName::None is a different enum and decodes to ["None"]. Input counts above the raw helper limit reject the whole call (BatchTooLarge: Registrar 8, Resolver 31, Primary 13). The current optimized path can return related records and their contract context together: liveRecords, destinations and primaryRecords use the corresponding row shapes above. Each reverseRecords row is null or [nameString, nodeBytes, generationBigInt, paymentOrNull], where paymentOrNull is null or [paymentDestination, explicitForwardBoolean]. A missing payment does not turn an election into a verified result. Lookup checks the current Registrar generation, namespace pointers and full destination before returning a display name. These helpers preserve input order and duplicates. Their outputs are raw observations for the verification process. Apps should use Universal Lookup’s public methods for identity answers.