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.
Contents
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 astokenid_utf8and used for every lookup. Unique within a namespace only. - Token id
- The Base64 form of a name. Three variants are stored:
tokenid_originas submitted,tokenidnormalised, andtokenid_utf8decoded. - 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
iduabefore 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
| Code | Meaning | Raised by |
|---|---|---|
ND | No such namespace. The deploy hash did not resolve | Any operation needing a namespace |
UO | Unsupported operation. Dispatch reached an unrecognised op | Dispatch |
IH | The operation needs a namespace hash and none was resolved | mint, transfer, renew, setrecord |
IND | The operation needs a term and none was supplied | mint, renew |
IDP | A deploy field exceeded its length limit, or was absent | deploy |
IR | Output 0 does not pay a namespace recipient | mint |
ITT | tokenid is not a non-empty list | mint |
ITC | coef is not an integer in 0 to 1000 | mint |
ITI | img is not a list where a list is required | mint |
ITD | dua is not an integer greater than zero | mint, renew |
ITID | The namespace's idua is not greater than zero | mint, renew |
IRL | No price entry for this name length and no fallback entry | mint, renew |
IRS | The discount signature did not verify against either accepted message | mint |
IRM | A submitted img entry did not match the derived URL. Below block 872200 only | mint |
IRV | Output 0's value is below the required payment | mint, renew |
UT | The block timestamp is before mintstart | mint |
OT | The block timestamp is at or after mintend | mint |
DM | Every requested name was already held with a live expiry | mint |
NM | The name has never been minted, or has no expiry | transfer, renew, setrecord |
NO | The creator is not the current owner | transfer, renew, setrecord |
OE | The term has already expired | transfer, renew, setrecord |
ID | Neither an address record nor a text record was produced | setrecord |
IDB | The primary flag was claimed but the btc record is not the creator | setrecord |
IT | Declared for an invalid token id at operation level | Declared, not reached by the handlers |
UE | An unexpected exception escaped a handler | Any |
Field-level codes
These appear in the status text as a prefix and mean the operation was excluded, not recorded as invalid.
| Code | Field | Cause |
|---|---|---|
NN | lim, dua, idua, mintstart, mintend, coef | Failed the numeric pattern, or would not convert to an integer of at least zero |
IT | tokenid | Not valid Base64, over 128 characters, a duplicate within a list, or a decoded name with a special character |
IH | hash | Not 64 hexadecimal characters |
IA | owner, toaddress, root, name | Not a decodable Bitcoin address, or a value containing a special character |
IAL | rec | Not a list, or a member that is not a decodable Bitcoin address |
IWLA | wla | Not a 66-character compressed key on secp256k1 beginning 02 or 03 |
IPC | pri | Not an object, a key that is not an integer, a duplicate key, or a value that is not an integer |
IP | prim | Not the string "true" or the string "false" |
IIM | imglp, imgf, sig, img | Wrong type. The first three must be strings, img must be a list of strings |
IBT | block_time | Not a timestamp. Indexer-internal |
3. Storage tables
| Table | Written when | Holds |
|---|---|---|
SRC101 | Every parsed operation, valid or not | The full operation record: transaction identifiers, block index, every payload field, the destination and its value, the block time and the status |
SRC101Valid | The operation was marked valid | The same shape. This is the table the owners projection is rebuilt from |
src101price | A record carries pri | One row per price entry, keyed by name length, with the price in satoshis and the owning deploy hash |
recipients | A record carries rec | The addresses a namespace will accept payment at |
owners | Any valid operation that changes an entry | The 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.
- Every row at or after the target block is deleted from
SRC101Valid,SRC101,src101priceandrecipients, alongside the stamp, transaction and block tables. - Caches are cleared, after the deletion rather than before.
- The owners table is rebuilt by replaying the surviving
SRC101Validrows 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 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
| Protocol | What it carries | Genesis | State |
|---|---|---|---|
| SRC-20 | Fungible tokens | 788041 | Account ledger, balance per address |
| SRC-721 | Composable stamp collections | 792370 | Stamp records, no separate ledger |
| SRC-101 | Names | 870652 | One 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.
- 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.
- No consensus check. SRC-101 contributes nothing to the ledger hash and has no external cross-check. Two implementations can diverge silently.
- 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.
- 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.
- 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.
limis dead weight. It enforces nothing and doubles as a presence sentinel, so a namespace deployed with a zero limit is silently unusable.- The rebuild path diverges from the live path. Previous owner, primary flag and image URL can differ after a rollback, as section 5 details.
- Records other than
ethare unproved. Abtcrecord is a well-formed address, not a controlled one. Atxtrecord is arbitrary text. - Term arithmetic ignores calendars. A year is exactly 31536000 seconds. Expiries drift against civil dates by roughly a day every four years.
- No mempool semantics. Nothing is reserved before confirmation, so racing for a lapsing name is a genuine race.
- 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.
- 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
btcortxtrecord as verified. Only theethrecord 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
- 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. - 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.
- P2WSH takes precedence with no multisig fallback.
- Read the destination value from output 0 on the multisig branch always, and on the P2WSH branch only from block 940000.
Classification
- Lower-case every top-level key before anything else.
- Require the protocol field to be
src-101, case-insensitively. - Require
keyburn == 1. - Reject everything below block 870652. Apply no upper bound.
- Accept both the Counterparty and the non-Counterparty branch, with no extra conditions on either.
Key sets
- Exact symmetric-difference matching for
deploy,transfer,setrecordandrenew, at every height. - Exact matching for
mintbelow block 872200, against the set that includesimg. - Containment matching for
mintat and above 872200, against the set withoutimg, permitting extra keys. - Discard an unrecognised operation with no record.
Fields
- Normalise the empty string to null for every field, first.
- Coerce the six numeric fields to integers of at least zero, preserving the string versus number asymmetry.
- Accept
primonly as the strings"true"and"false". - Validate Base64 with the standard alphabet, a length that is a multiple of four, and a 128-character cap.
- Reject decoded names containing space separators, format characters or the punctuation set.
- Reject duplicate names within a list after decoding.
- Abandon the whole operation on any field failure.
Operations
- Deploy: enforce the six length limits, substitute the maximum for a missing
mintend, treat a zerolimas an unresolvable namespace. - Deploy: do not resolve a namespace from within its own block.
- 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.
- Transfer: owner check, expiry check, unchanged expiry, cleared records, cleared primary flag.
- Renew: owner check, strict expiry check with no grace period, no discount, new expiry as old expiry plus term.
- 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
- Write both the operation table and the valid table, and key state on validity rather than on the parse flag.
- Keep expired entries rather than deleting them, and record the previous holder on a re-mint.
- On rollback, purge by block index and rebuild the register by replaying the valid table.
- Contribute nothing to the ledger hash, and do not expect an external cross-check to catch a divergence.