Reference · Indexer semantics · btc_stamps 1.9.3

SRC-101 reference

What an implementer needs after the specification: the vocabulary, every status code, the five tables that hold state, how ordering, confirmation and reorgs actually behave, what the protocol cannot do, and a checklist to work through before you claim your indexer is correct.


1. Terminology

Namespace
The scope created by one deploy, identified by that deploy's transaction hash. Called the deploy hash everywhere in the code.
Name
The decoded, lower-cased UTF-8 form of a tokenid. Stored as tokenid_utf8 and used for every lookup. Unique within a namespace only.
Token id
The Base64 form of a name. Three variants are stored: tokenid_origin as submitted, tokenid normalised, and tokenid_utf8 decoded.
Entry
One row in the owners table: one name in one namespace, with an owner, a previous owner, an expiry, records and a primary flag.
Creator
The address that funded input 0. Every ownership check compares against this.
Destination
The address of output 0. Mint and renew require it to be a namespace recipient.
Destination value
The satoshi value of output 0. The payment checks measure against this, and it is read differently by carrier below block 940000.
Term, dua
A count of years of 31536000 seconds. Rounded up to a multiple of the namespace's idua before use.
Granularity, idua
The namespace's term unit, also in years. Must be greater than zero or no mint or renew can succeed.
Rounds
The rounded term divided by the granularity. The multiplier applied to the unit price.
Coefficient, coef
A discount in parts per thousand, 0 to 1000. Ignored unless a signature from the namespace whitelist key proves it.
Whitelist key, wla
The namespace's compressed secp256k1 public key, used to verify discount authorisations.
Recipients, rec
The addresses a namespace will accept payment at. Stored de-duplicated, with order not preserved.
Primary
A per-address, per-namespace flag marking one name as the holder's main one. Setting it clears it elsewhere.
Excluded
A payload that never became an SRC-101 operation: no row, no status code, no trace.

2. Status codes

Two families exist and they behave differently. Field-level codes are assigned by the validator before any handler runs, and they cause the operation to be abandoned. Operation-level codes are assigned by a handler, and they produce a recorded invalid operation.

Operation-level codes

Assigned by the five handlers. Only ND and UO flag the parse as hard-invalid
CodeMeaningRaised by
NDNo such namespace. The deploy hash did not resolveAny operation needing a namespace
UOUnsupported operation. Dispatch reached an unrecognised opDispatch
IHThe operation needs a namespace hash and none was resolvedmint, transfer, renew, setrecord
INDThe operation needs a term and none was suppliedmint, renew
IDPA deploy field exceeded its length limit, or was absentdeploy
IROutput 0 does not pay a namespace recipientmint
ITTtokenid is not a non-empty listmint
ITCcoef is not an integer in 0 to 1000mint
ITIimg is not a list where a list is requiredmint
ITDdua is not an integer greater than zeromint, renew
ITIDThe namespace's idua is not greater than zeromint, renew
IRLNo price entry for this name length and no fallback entrymint, renew
IRSThe discount signature did not verify against either accepted messagemint
IRMA submitted img entry did not match the derived URL. Below block 872200 onlymint
IRVOutput 0's value is below the required paymentmint, renew
UTThe block timestamp is before mintstartmint
OTThe block timestamp is at or after mintendmint
DMEvery requested name was already held with a live expirymint
NMThe name has never been minted, or has no expirytransfer, renew, setrecord
NOThe creator is not the current ownertransfer, renew, setrecord
OEThe term has already expiredtransfer, renew, setrecord
IDNeither an address record nor a text record was producedsetrecord
IDBThe primary flag was claimed but the btc record is not the creatorsetrecord
ITDeclared for an invalid token id at operation levelDeclared, not reached by the handlers
UEAn unexpected exception escaped a handlerAny

Field-level codes

These appear in the status text as a prefix and mean the operation was excluded, not recorded as invalid.

Assigned by the validator before dispatch
CodeFieldCause
NNlim, dua, idua, mintstart, mintend, coefFailed the numeric pattern, or would not convert to an integer of at least zero
ITtokenidNot valid Base64, over 128 characters, a duplicate within a list, or a decoded name with a special character
IHhashNot 64 hexadecimal characters
IAowner, toaddress, root, nameNot a decodable Bitcoin address, or a value containing a special character
IALrecNot a list, or a member that is not a decodable Bitcoin address
IWLAwlaNot a 66-character compressed key on secp256k1 beginning 02 or 03
IPCpriNot an object, a key that is not an integer, a duplicate key, or a value that is not an integer
IPprimNot the string "true" or the string "false"
IIMimglp, imgf, sig, imgWrong type. The first three must be strings, img must be a list of strings
IBTblock_timeNot a timestamp. Indexer-internal

3. Storage tables

The five tables that hold SRC-101 state
TableWritten whenHolds
SRC101Every parsed operation, valid or notThe full operation record: transaction identifiers, block index, every payload field, the destination and its value, the block time and the status
SRC101ValidThe operation was marked validThe same shape. This is the table the owners projection is rebuilt from
src101priceA record carries priOne row per price entry, keyed by name length, with the price in satoshis and the owning deploy hash
recipientsA record carries recThe addresses a namespace will accept payment at
ownersAny valid operation that changes an entryThe current register: namespace, token id in all three forms, image URL, previous owner, owner, text record, expiry, resolved Bitcoin and Ethereum addresses, and the primary flag

The owners row is keyed by the protocol string, the deploy hash and the normalised token id joined together, and it is written as an upsert. An entry is never deleted by an operation: a name that expires keeps its row with its old owner and old expiry, and a later mint overwrites that row.

A namespace lookup resolves nine values from the deploy: the limit, the price object, the mint window start and end, the recipient list, the whitelist key, the image prefix and suffix, and the granularity. It is cached, then read from the current block, then read from the valid table. A resolved limit of zero means "not found" at every layer, which is why rule 40 matters.

4. Indexer semantics

Ordering

Operations within a block are processed in transaction order, and a name minted earlier in a block is visible to a later transaction in the same block through an in-block lookup that runs before the database is consulted. Across blocks, ordering is block index then transaction index.

The one exception is namespace resolution. The in-block namespace lookup matches on a field that a deploy row never carries, so a namespace deployed in block N cannot be resolved until block N is committed. A mint in the deploy's own block fails with ND.

Confirmation

An SRC-101 operation takes effect when the block containing it is processed by the indexer. There is no partial state, no pending state and no probabilistic state within the protocol itself. The Bitcoin Universe capability registry records no confirmation policy for SRC-101, so any depth requirement you see is an application decision rather than a documented one.

Mempool

There is no mempool state. Parsing runs inside block processing. An unconfirmed transaction has no SRC-101 meaning: a name is not reserved by a broadcast, and two people can both broadcast a mint for the same free name. Whichever lands first in a confirmed block wins, and the other is silently dropped from its list, or fails with DM if it was the only name.

The ledger hash

The block ledger hash is computed from valid stamps and the SRC-20 operation string. SRC-101 contributes nothing. The indexer also cross-checks its SRC-20 ledger hash against an external reference and raises a critical alert on a mismatch. There is no equivalent check for SRC-101, so a divergence between two SRC-101 implementations can persist indefinitely without any signal.

5. Reorg and the owners rebuild

Rollback is uniform across the indexer. A detected chain reorganisation triggers a rollback ten blocks deeper than the divergence, as a safety margin.

  1. Every row at or after the target block is deleted from SRC101Valid, SRC101, src101price and recipients, alongside the stamp, transaction and block tables.
  2. Caches are cleared, after the deletion rather than before.
  3. The owners table is rebuilt by replaying the surviving SRC101Valid rows in block and transaction order.

The owners table is not purged by block index. It is replaced wholesale by the replay, which is what keeps it consistent. That rebuild also runs at indexer startup, and is skipped when the replay matches what is already stored.

The replay is a second implementation of the state machine

The live path and the rebuild path compute the register separately, and they do not agree in every detail. Verified differences in the rebuild:

  • A mint records no previous owner. The live path records the holder whose term had expired.
  • A transfer does not clear the primary flag. The live path writes it false on a newly created row.
  • Setting a primary does not clear the flag from the holder's other names. The live path does.
  • Where a stored image URL is absent, the rebuild derives one from the namespace prefix and suffix rather than leaving it empty.

The consequence is that the register after a rollback can differ from the register before it, in fields that are not the owner or the expiry. If your application reads the previous owner or the primary flag, treat those as advisory rather than authoritative.

6. The Counterparty relationship

SRC-101 inherits the Bitcoin Stamps classification path, which resolves a Counterparty asset identifier for a transaction where one exists. The gate is a plain disjunction: an SRC-101 payload with an asset identifier, or an SRC-101 payload without one. Both are accepted.

Two things follow that people get wrong in both directions:

  • SRC-101 never stops accepting Counterparty-carried operations. SRC-20 ignores them from block 796000, and there is no equivalent constant for SRC-101. Saying SRC-101 works the way SRC-20 does here is wrong.
  • SRC-101 has no Counterparty history to replay. Nothing exists below block 870652, which is far above every Counterparty-era constant in the codebase. An indexer needs issuance data available for the classification gate, but needs no Counterparty state for SRC-101 correctness.

The Counterparty branch for SRC-101 also carries none of the extra conditions the SRC-20 branch carries: no requirement that the asset identifier begins with A, and no requirement that supply is zero.

7. The Stamps protocol family

The three sub-protocols the Stamps carrier recognises
ProtocolWhat it carriesGenesisState
SRC-20Fungible tokens788041Account ledger, balance per address
SRC-721Composable stamp collections792370Stamp records, no separate ledger
SRC-101Names870652One entry per name per namespace

All three share the same carrier, the same stamp: prefix and the same keyburn requirement, and all three are recognised from the same protocol field. None of them shares state with any other. A transaction is classified as exactly one.

The SRC-20 documentation is the sibling of this site and covers the shared carrier in the same detail: bitcoinuniverseio.github.io/src-20/specification.html. The two sites deliberately describe the carrier twice so that each stands alone.

8. Size and fee considerations

The protocol imposes two costs. The first is the Bitcoin fee for carrying the payload, which scales with payload size and carrier choice. The second is the namespace payment, which mint and renew require and which is fixed by the deploy's price list.

  • A multisig data output carries 62 bytes: a 1-of-3 bare multisig whose third key is a burn key, with the two data-bearing keys contributing 31 bytes each.
  • A P2WSH data output carries 32 bytes, and only outputs after the first one count, since output 0 is the payment.
  • Both carriers add a two-byte length and a six-byte prefix to the JSON before chunking.
  • Payloads are not compressed. Field names are short for a reason.

Deploy is by far the largest operation, because all sixteen fields are mandatory and several are long strings. Renew is the smallest. The guide gives computed counts for each operation.

Below block 940000 the P2WSH branch reports a destination value of zero, so a paid operation carried over P2WSH will fail its payment check regardless of what was actually paid. Bare multisig is the only viable carrier for a mint or a renew below that height.

9. Limitations

Stated plainly, because most of these have no workaround.

  1. No trade path exists in any Bitcoin Universe product. The capability registry records no marketplace entry for SRC-101 at all. There is no escrow primitive in the protocol either: a name is a database row, not an output that can be locked, so a non-custodial swap cannot be constructed the way it can for a UTXO-native asset.
  2. No consensus check. SRC-101 contributes nothing to the ledger hash and has no external cross-check. Two implementations can diverge silently.
  3. No grace period on expiry. The moment the block timestamp reaches the expiry, renewal, transfer and record-setting all stop working, and the name is claimable by anyone.
  4. A mint cannot be made atomic across several names. Taken names are dropped and the rest proceed. There is no all-or-nothing mode and no refund.
  5. Exclusion is invisible. A payload rejected by key matching or field validation leaves no record. There is nothing to look up and nothing to explain the failure to a user, beyond a transaction that appears to have done nothing.
  6. lim is dead weight. It enforces nothing and doubles as a presence sentinel, so a namespace deployed with a zero limit is silently unusable.
  7. The rebuild path diverges from the live path. Previous owner, primary flag and image URL can differ after a rollback, as section 5 details.
  8. Records other than eth are unproved. A btc record is a well-formed address, not a controlled one. A txt record is arbitrary text.
  9. Term arithmetic ignores calendars. A year is exactly 31536000 seconds. Expiries drift against civil dates by roughly a day every four years.
  10. No mempool semantics. Nothing is reserved before confirmation, so racing for a lapsing name is a genuine race.
  11. No subdomains, no delegation, no reverse resolution primitive. The primary flag is the closest thing to reverse resolution and it is scoped to one namespace.
  12. Namespaces are not discoverable in-protocol. There is no registry of namespaces. You must be given a deploy hash out of band, and nothing in the protocol distinguishes a legitimate namespace from a copy.

10. Security considerations

For implementers

  • Do not add a multisig fallback when P2WSH data fails. The exclusion is consensus-load-bearing and the source marks it as such. Adding a fallback forks the register.
  • Reproduce the exclusion behaviour exactly. An implementation that is lenient about extra keys will accept operations the reference rejects and will diverge on the first non-conforming payload.
  • Reproduce the numeric asymmetry. A JSON number with a fractional part truncates; the equivalent string is rejected. Normalising both to the same behaviour changes the register.
  • Compare against decoded names, never against Base64. Two encodings of the same name must collide.
  • Round the term before signing checks and before storing. The discount signature covers the rounded value.
  • Do not implement the declared address regular expressions. They are unused, and the Bitcoin one would reject every Bech32 address.

For applications

  • Show the namespace with every name. A name without its deploy hash is ambiguous by design.
  • Show the expiry, and show it prominently near a purchase. A transfer conveys the remaining term and nothing more.
  • Warn before a mint of several names. Explain that taken names are dropped and the payment is computed over the full list.
  • Do not present a btc or txt record as verified. Only the eth record carries a signature the indexer checks.
  • Detect homoglyphs before display. The character rules block whitespace and format characters, not lookalikes.
  • Treat a single explorer as a single opinion. With no consensus check, agreement between implementations is not guaranteed.

For holders

  • Renew with margin. The comparison is against a block timestamp you do not control.
  • Verify the decoded name, not the Base64 you were given.
  • Verify the payment address against the namespace's recipient list before signing.
  • Re-establish your records after receiving a transferred name. They will be empty.

11. Implementation checklist

Work through this before claiming an SRC-101 indexer is correct. Each item has a corresponding vector on the test vectors page.

Carrier

  1. Bare multisig: concatenate each collected key minus its first and last byte, ARC4-decrypt with the byte-reversed outpoint hash of input 0, check bytes 2 to 7 against stamp:, honour the two-byte length.
  2. P2WSH: collect 32-byte witness programs from outputs after the first, join, strip trailing zeros, honour the length, check the prefix, force keyburn to 1.
  3. P2WSH takes precedence with no multisig fallback.
  4. Read the destination value from output 0 on the multisig branch always, and on the P2WSH branch only from block 940000.

Classification

  1. Lower-case every top-level key before anything else.
  2. Require the protocol field to be src-101, case-insensitively.
  3. Require keyburn == 1.
  4. Reject everything below block 870652. Apply no upper bound.
  5. Accept both the Counterparty and the non-Counterparty branch, with no extra conditions on either.

Key sets

  1. Exact symmetric-difference matching for deploy, transfer, setrecord and renew, at every height.
  2. Exact matching for mint below block 872200, against the set that includes img.
  3. Containment matching for mint at and above 872200, against the set without img, permitting extra keys.
  4. Discard an unrecognised operation with no record.

Fields

  1. Normalise the empty string to null for every field, first.
  2. Coerce the six numeric fields to integers of at least zero, preserving the string versus number asymmetry.
  3. Accept prim only as the strings "true" and "false".
  4. Validate Base64 with the standard alphabet, a length that is a multiple of four, and a 128-character cap.
  5. Reject decoded names containing space separators, format characters or the punctuation set.
  6. Reject duplicate names within a list after decoding.
  7. Abandon the whole operation on any field failure.

Operations

  1. Deploy: enforce the six length limits, substitute the maximum for a missing mintend, treat a zero lim as an unresolvable namespace.
  2. Deploy: do not resolve a namespace from within its own block.
  3. Mint: recipient membership, list-form token id, coefficient range, image handling by height, term rounding, price lookup with the zero-length fallback, signature verification against both message forms, payment comparison, mint window, per-name drop of live entries, expiry as block time plus term.
  4. Transfer: owner check, expiry check, unchanged expiry, cleared records, cleared primary flag.
  5. Renew: owner check, strict expiry check with no grace period, no discount, new expiry as old expiry plus term.
  6. Setrecord: owner check, expiry check, the type-to-field mapping, the Ethereum signature recovery over the reversed previous transaction hash, the primary constraint, the primary clear across the namespace, and record merging rather than replacement.

State and recovery

  1. Write both the operation table and the valid table, and key state on validity rather than on the parse flag.
  2. Keep expired entries rather than deleting them, and record the previous holder on a re-mint.
  3. On rollback, purge by block index and rebuild the register by replaying the valid table.
  4. Contribute nothing to the ledger hash, and do not expect an external cross-check to catch a divergence.