No trust required

Take nobody's word for it.

Depth is a subtraction between two block heights, and your own node already knows both. The first three sections need nothing but bitcoind and a shell. The indexer sections after them are optional, and none of them ask you to take a server's word for anything.

  • Depth

    One call to gettxout. If the output is still there, its confirmation count carries the answer.

  • Identity

    One SHA-256 over an ASCII tag and a few bytes you already have. No secrets and no server.

  • State

    A pure function of confirmed blocks, so two honest indexers at the same height must agree exactly.

Before you start

Local only
A Bitcoin node
Bitcoin Core with RPC access. Two of the commands here want -txindex, and every other one works without it.
A POSIX shell
Plus jq for reading JSON, and xxd and sha256sum for the hashes. macOS substitutions are noted where they differ.
An indexer, if you want one
Three of the sections below use one. Everything before them runs on your own node, and nothing an indexer reports is taken as final.

Angle brackets mark the parts you replace.

01 The only step that matters

Read depth from any Bitcoin node.

An unspent output reports how many confirmations it has, and the block that created it counts as the first. Depth is that count minus one, with no index and no third party anywhere in the answer.

Depth of a carrier
# the whole check, in one line
bitcoin-cli gettxout <txid> <vout> true | jq '.confirmations - 1'

# null output means the carrier has been spent
# a spent outpoint has no depth, because the output no longer exists
Depth, value and tier
#!/bin/sh
# usage: patina-depth <txid> <vout>
OUT=$(bitcoin-cli gettxout "$1" "$2" true)
if [ -z "$OUT" ] || [ "$OUT" = "null" ]; then
  echo "spent or unknown, depth is not defined for this outpoint"
  exit 1
fi

DEPTH=$(( $(printf '%s' "$OUT" | jq -r '.confirmations') - 1 ))
VALUE=$(printf '%s' "$OUT" | jq -r '.value')

if   [ "$DEPTH" -ge 210000 ]; then TIER=Elder
elif [ "$DEPTH" -ge 105120 ]; then TIER=Oxide
elif [ "$DEPTH" -ge  52560 ]; then TIER=Bronze
elif [ "$DEPTH" -ge  26280 ]; then TIER=Umber
elif [ "$DEPTH" -ge  12960 ]; then TIER=Verdigris
elif [ "$DEPTH" -ge   4032 ]; then TIER=Cast
elif [ "$DEPTH" -ge   1008 ]; then TIER=Sheen
else                               TIER=Raw
fi

echo "depth $DEPTH blocks"
echo "tier  $TIER"
echo "value $VALUE BTC"

Seven thresholds and a fallback for everything below the first one: that is the whole tier ladder. Nothing else decides a tier, which is why a shell script can compute one. The names and the arithmetic behind them are set out on the tiers page.

The long way, if you prefer heights
# needs -txindex, or the transaction has to be in your wallet
TIP=$(bitcoin-cli getblockcount)
BLOCKHASH=$(bitcoin-cli getrawtransaction <txid> true | jq -r '.blockhash')
BIRTH=$(bitcoin-cli getblockheader "$BLOCKHASH" | jq -r '.height')
echo $(( TIP - BIRTH ))

What this proves

That the outpoint has not moved since the height you just computed. Any node on earth reaches the same number from the same chain.

What it does not prove

Who holds the key, or that the same person held it the whole time. For liveness of the key, ask the holder for a signed attestation, which is the third command in the identifiers section below.

02 Bytes, not descriptions

Read the marker with your own eyes.

A reveal transaction announces itself in an OP_RETURN output. Pull the nulldata outputs out of it, then check the bytes against the layout on the protocol page.

Find the marker script
bitcoin-cli getrawtransaction <reveal_txid> true \
  | jq -r '.vout[] | select(.scriptPubKey.type == "nulldata")
           | "vout \(.n)  \(.scriptPubKey.hex)"'

# the marker is the lowest vout whose push starts with 50544e41
# if two outputs both start with 50544e41 the marker is void

Two markers cancel each other

A transaction carrying more than one payload that begins with 50544e41 has a void marker. There is no tie break and no first wins rule, so a second copy destroys the claim rather than duplicating it.

What each field of a SEED payload is

Magic, 4 bytes
50544e41, the ASCII letters PTNA. A push that does not open with these bytes is not a PATINA marker at all.
Version, 1 byte
01. Any other value is MARKER_UNKNOWN_VERSION and the marker creates nothing, so a byte you do not recognise is a payload you should not guess at.
Op, 1 byte
01 marks a SEED, the claim that brings an artifact into existence. 02 marks a KEEP, which routes carriers onto successor outputs. Anything else is MARKER_UNKNOWN_OP.
Salt, 16 bytes
The random value that was hidden inside the commit. It is what makes the commitment unguessable while it waits.
Flags, 1 byte
00, and nothing else. Every bit is reserved at marker version 1 and has to be zero, so any other flags byte is SEED_BAD_GRAMMAR.
Carrier vout, 1 byte
Which output of this reveal transaction becomes the carrier. Depth starts counting at the height of the block that confirms it. One byte, so a SEED cannot name an output past 255.
Split a SEED marker into fields
HEX=6a1850544e4101013f8a1c5d9e04b7226ce14093aa57db080000

printf '%s' "$HEX" | cut -c1-2    # 6a        OP_RETURN
printf '%s' "$HEX" | cut -c3-4    # 18        push 24 bytes
printf '%s' "$HEX" | cut -c5-12   # 50544e41  PTNA
printf '%s' "$HEX" | cut -c13-14  # 01        version
printf '%s' "$HEX" | cut -c15-16  # 01        SEED
printf '%s' "$HEX" | cut -c17-48  #           16 byte salt
printf '%s' "$HEX" | cut -c49-50  # 00        flags
printf '%s' "$HEX" | cut -c51-52  # 00        carrier vout

# the hex above is the worked example from the protocol page,
# not a marker from any real transaction

Four plus one plus one plus sixteen plus one plus one is twenty four bytes, which is the 18 in the push opcode. If those two numbers do not match, stop reading the payload.

03 Names you can derive

Recompute the identifiers.

Each derivation is a single SHA-256 over an ASCII tag and some bytes. If an indexer hands you an artifact id, you can confirm it without asking anyone whether it is right.

Artifact id

Artifact id from the reveal outpoint
TXID=<reveal txid as your explorer shows it, 64 hex characters>
VOUT=<carrier vout, decimal>

# txid in internal byte order, which is the display order reversed
WIRE=$(printf '%s' "$TXID" | fold -w2 | tac | tr -d '\n')

# carrier vout as four little endian bytes
VOUTLE=$(printf '%08x' "$VOUT" | fold -w2 | tac | tr -d '\n')

{ printf 'PTNA/artifact'; printf '%s%s' "$WIRE" "$VOUTLE" | xxd -r -p; } | sha256sum

# on macOS, replace tac with "tail -r" and sha256sum with "shasum -a 256"

The byte order reversal is the step people get wrong. An explorer shows a txid backwards from the way it sits on the wire, and the hash is taken over the wire order.

Commit commitment

Commitment from the key and the salt
XONLY=<claimant x only public key, 64 hex characters>
SALT=<the 16 byte salt from the SEED payload, 32 hex characters>

{ printf 'PTNA/commit'; printf '%s%s' "$XONLY" "$SALT" | xxd -r -p; } | sha256sum

# the result must equal the 32 bytes pushed in the commit leaf that the
# reveal transaction spent: the 68 byte reduced-data leaf that new
# construction uses, or the 70 byte legacy leaf for older commits

This is the check that ties a claim to the key that made it. If the recomputed commitment does not appear in the spent leaf, the reveal does not belong to that commit.

Attestation message

The exact string a holder signs
# build the exact string the holder has to sign with BIP-322
printf 'PTNA/attest%s%s' <artifact_id_hex> <block_hash_hex>

# pick a recent block hash yourself, so an old signature cannot be replayed
# a valid signature over this string, from the key behind the carrier,
# tells you the key is alive right now
# it still does not tell you who has been holding it

Choose the block hash yourself

A signature over a string you chose, containing a hash that did not exist yesterday, cannot have been made yesterday. If the holder supplies the block hash instead of you, the signature proves much less.

04 Optional, and never final

Ask an indexer, then check its answer.

An indexer is a convenience, not an authority. Use it to find things quickly, then confirm the parts that matter against your own node.

The commands below use one shell variable. Set it to the base URL of an indexer you trust. The line under this paragraph prints whichever one this site is configured against.

Set the base URL
export PATINA_API="https://your-indexer.example.org/patina"

If that line shows an example host, this site has no indexer configured. The routes below are the ones the frozen indexer API contract names, so any conforming indexer answers them under its own base path.

Identity and sync state
curl -s "$PATINA_API/status" | jq '{network, protocol_id, spec_sha256,
                                   tip_height, indexed_height, synced,
                                   parser_version, indexer_version}'

# check spec_sha256 against the hash of the specification you read
# check tip_height against your own node: bitcoin-cli getblockcount
One artifact, one carrier, one address
curl -s "$PATINA_API/artifacts/<artifact_id>" \
  | jq '{status, founding, depth, tier, tier_name,
         next_tier, blocks_to_next_tier, carrier, rings}'

curl -s "$PATINA_API/carriers/<txid>/<vout>" | jq

curl -s "$PATINA_API/addresses/<address>/holdings" | jq

# now confirm the depth it reported, with your own node:
# bitcoin-cli gettxout <txid> <vout> true | jq '.confirmations - 1'
Before you spend anything
curl -s -X POST "$PATINA_API/safety/outpoints" \
  -H 'content-type: application/json' \
  -d '{"outpoints":["<txid>:<vout>"]}' | jq

# tells you whether an outpoint is a carrier or a commit before a wallet
# picks it as an ordinary coin and resets something by accident
# the endpoint stores nothing

Coin selection is the real hazard

A wallet that does not know about carriers will happily spend one to pay a fee. That spend is valid Bitcoin, the depth goes to zero, and nobody can undo it. The risks page covers what else can go wrong.

05 The strongest position

Run the index yourself.

Your own index sitting next to your own node answers to nobody. The indexer lives in a separate repository from the protocol library.

Clone it
git clone https://github.com/bitcoinuniverse/index-patina
cd index-patina

# the README in that repository is the authoritative setup guide
# it needs a Bitcoin node RPC endpoint and a deployment record for the
# network you are indexing, and nothing else

We are not going to print configuration flags here that could drift out of date. The repository is the source of truth for how to run it.

06 Two answers, one chain

Set two indexers against each other.

PATINA state is a pure function of the confirmed chain read in block order. Two honest indexers at the same indexed height must agree exactly. Disagreement means one of them is wrong, and that is worth knowing.

Compare counters at the same height
A=<base URL of the first indexer>
B=<base URL of the second indexer>

diff \
  <(curl -s "$A/status" | jq -S '{spec_sha256, indexed_height, counters}') \
  <(curl -s "$B/status" | jq -S '{spec_sha256, indexed_height, counters}')

# if indexed_height differs, wait for the slower one and run it again
# if spec_sha256 differs, they are not running the same specification
# if the counters differ at the same height, one of them has a bug
Compare one artifact in detail
ID=<artifact_id>

diff \
  <(curl -s "$A/artifacts/$ID" | jq -S '{status, founding, carrier, rings}') \
  <(curl -s "$B/artifacts/$ID" | jq -S '{status, founding, carrier, rings}')

# depth is deliberately left out of this comparison, because it is a
# function of the tip and the two indexers may be at different tips

About state roots

The specification defines a state root as a single SHA-256 over the tag PTNA/state and a canonical snapshot encoding, so two indexers holding the same state at the same height produce the same 32 bytes. The frozen API contract does not name a field that exposes it, so where an implementation publishes its root is up to that implementation. Check the indexer's own documentation. Until you find it, the counter comparison above is the check that works against any conforming indexer.

07 When numbers collide

If the answers disagree, the chain wins.

Trust your node over any indexer, and trust an indexer you run over one somebody else runs. If an indexer's depth does not match gettxout, the indexer is wrong.

The order in which answers are trusted Three sources stacked from strongest to weakest: your own Bitcoin node, an index you run yourself, and an index somebody else runs. Each lower source counts only while it agrees with the ones above it. Which answer wins STRONG WEAK Your own Bitcoin node gettxout, getblockcount, getblockheader. This is the chain, not a report about it. An index you run Derived from the same blocks, by software you started and can stop. An index somebody else runs Quick to ask, and worth nothing on its own until it agrees with the two above.
The ordering is not a policy we are asking you to accept. It is the order in which a disagreement actually resolves, because only one of the three is the chain.

Report a mismatch

Open an issue on the indexer repository with the outpoint and both answers. The outpoint is enough for anyone to reproduce it.

A wrong index changes nothing

An indexer reporting the wrong depth has not altered your artifact. The carrier is where it was, the rings are where they were, and a corrected index recomputes both from the same blocks.

What we publish about our own indexer, including how it fails and what it cannot reach, is on the transparency page.

08 Go further

Where the harder checks live.