Skip to content

tbc: add ordinal indexer#1024

Open
marcopeereboom wants to merge 28 commits into
mainfrom
marco_ordinals
Open

tbc: add ordinal indexer#1024
marcopeereboom wants to merge 28 commits into
mainfrom
marco_ordinals

Conversation

@marcopeereboom
Copy link
Copy Markdown
Contributor

@marcopeereboom marcopeereboom commented May 12, 2026

depends on #1035

Add ordinal indexer to TBC that tracks individual satoshi ownership via sat ranges and indexes Bitcoin inscriptions. Follows the existing Indexer/indexer framework pattern, modeled on zkindexer.go.

Database:

  • New LevelDB "ordinals" with prefix-separated keys: 'r' (sat ranges per UTXO), 'i' (inscription by ID), 's' (inscribed sat → outpoint), 'a' (sat → all inscriptions), 'n' (inscriptions by block).
  • OrdinalKey type, 7 new Database interface methods.
  • Database version bump 5 → 6.

Indexer:

  • FIFO sat range redistribution engine.
  • Inscription envelope parser supporting all tags: content type, pointer, parent, delegate, metaprotocol.
  • Cursed inscription detection (5 pre-jubilee rules).
  • Flag-driven variable-length value encoding.
  • Inscribed sat movement tracking via DB range scan.
  • Zero dependency on txindex during wind/unwind.

@marcopeereboom marcopeereboom requested a review from a team as a code owner May 12, 2026 13:50
@github-actions github-actions Bot added area: tbc This is a change to TBC (Tiny Bitcoin) changelog: required This pull request must update the CHANGELOG.md file or explicitly be marked with changelog: skip labels May 12, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

@github-actions github-actions Bot added area: docs This is a change to documentation changelog: done This pull request includes an appropriate update to CHANGELOG.md. and removed changelog: required This pull request must update the CHANGELOG.md file or explicitly be marked with changelog: skip labels May 12, 2026
@marcopeereboom marcopeereboom added type: feature This adds new functionality size: XXL This change is extremely large (+/- 1000+). Changes this large should be split into multiple PRs labels May 14, 2026
@github-actions github-actions Bot added the area: hemictl This is a change to hemictl label May 14, 2026
@marcopeereboom marcopeereboom force-pushed the marco_ordinals branch 2 times, most recently from c2d4b4f to 34cd7b2 Compare May 18, 2026 15:25
Copy link
Copy Markdown
Contributor

@AL-CT AL-CT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nits and comments

Comment thread database/tbcd/level/level.go Outdated
// Find first 's' entry.
var startKey tbcd.OrdinalKey
startKey[0] = 's'
it := ordDB.NewIterator(nil, nil)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this function can be simplified by using util.BytesPrefix(startKey[:]) and getting the first and last entries for the bounded iterator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — simplified OrdinalInscribedSatBounds to use util.BytesPrefix with First()/Last() instead of manual seek/prev logic.

Comment thread service/tbc/ordinalindex.go Outdated
if !i.inscCheckDone {
i.inscCheckDone = true
minSat, maxSat, err := i.g.db.OrdinalInscribedSatBounds(ctx)
if err == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check with errors.Is(err, database.ErrNotFound)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — the sat range computation code that contained this path was removed in the ordinals2 refactor. Sat ranges are now computed on demand via backward chain walk, not stored at index time.

Comment thread service/tbc/ordinalindex.go Outdated
Comment on lines +258 to +260
sort.Slice(blockInscribedSats, func(a, b int) bool {
return blockInscribedSats[a] < blockInscribedSats[b]
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can be simplified as

Suggested change
sort.Slice(blockInscribedSats, func(a, b int) bool {
return blockInscribedSats[a] < blockInscribedSats[b]
})
slices.Sort(blockInscribedSats)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moot — sort.Slice on blockInscribedSats was removed with the sat range computation code in the ordinals2 refactor.

Comment thread service/tbc/ordinalindex.go Outdated
Comment on lines +361 to +362
rk := ordinalRangeKey(op)
cache[rk] = nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
rk := ordinalRangeKey(op)
cache[rk] = nil
i.putRange(cache, outpoint, nil)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moot — putRange removed with the sat range computation code.

func makeInscriptionID(txHash *chainhash.Hash, inputIdx uint32) [36]byte {
var id [36]byte
copy(id[:32], txHash[:])
binary.LittleEndian.PutUint32(id[32:], inputIdx)
Copy link
Copy Markdown
Contributor

@AL-CT AL-CT May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Little Endian here part of the ord protocol? If so, maybe a small comment here is good

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added comment: "Inscription IDs use little-endian index per the ord protocol."

Comment thread service/tbc/ordinalindex_design.go Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this to the benchmark test file? Or create a .md file like zkindexers.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved to ordinalindex_design.md.

Comment thread service/tbc/ordinalindex_bench_test.go Outdated
Comment on lines +43 to +44
b.ResetTimer()
for i := 0; i < b.N; i++ {
Copy link
Copy Markdown
Contributor

@AL-CT AL-CT May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this and the other benchmark loops can be simplified as:

Suggested change
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moot — benchmark file removed.

Comment thread service/tbc/tbc.go Outdated

if utxoHH.Hash.IsEqual(&bhb.Hash) && txHH.Hash.IsEqual(&bhb.Hash) &&
!s.indexing && !blksMissing {
// If keystone and zk indexers are disabled we are synced.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
// If keystone and zk indexers are disabled we are synced.
// If keystone, zk, and ordinals indexers are disabled we are synced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — comment updated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if we could fit these tests in level_test.go, or at the very least TestDbUpgradeV6, since that's where the other upgrade tests are.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — keeping ordinal DB tests in ordinal_test.go for now. Will move TestDbUpgradeV6 to level_test.go with the other upgrade tests in a follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — moved TestDbUpgradeV6 to level_test.go alongside TestDbUpgradeV5 and TestDbUpgradeV4Errors.

Comment thread service/tbc/ordinalindex.go Outdated
Comment on lines +443 to +449
for txOutIdx, outRanges := range outputRanges {
for _, sr := range outRanges {
if inscribedSat >= sr.Start && inscribedSat < sr.Start+sr.Count {
outpoint := tbcd.NewOutpoint(*tx.Hash(), txOutIdx)
cache[ordinalSatKey(inscribedSat)] = outpoint[:]
i.trackInscribedSat(inscribedSat)
goto satFound
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not skip any new inscriptions on a sat that ends up going to fees? It looks like the inscribedFeeSats we return here and is used in windBlock to deal with this only verifies existing inscriptions, not new ones

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — the entire precomputed sat range model was replaced. Inscriptions are stored at index time without sat numbers. Sat numbers are computed on demand by tracing backward through the spending chain to coinbase. Fee region sats are handled correctly by computing per-tx fee amounts at the coinbase level.

@joshuasing joshuasing added this to the v2.1.0 milestone May 19, 2026
Comment thread service/tbc/ordinalquery.go Fixed
marcopeereboom added a commit that referenced this pull request May 19, 2026
- Add comment documenting that inscription IDs use little-endian
  index per the ord protocol (AL-CT #5)
- Move ordinalindex_design.go to ordinalindex_design.md (AL-CT #11)
- Simplify OrdinalInscribedSatBounds using util.BytesPrefix and
  First()/Last() instead of manual seek logic (AL-CT #1)
marcopeereboom added a commit that referenced this pull request May 19, 2026
- Add comment documenting that inscription IDs use little-endian
  index per the ord protocol (AL-CT #5)
- Move ordinalindex_design.go to ordinalindex_design.md (AL-CT #11)
- Simplify OrdinalInscribedSatBounds using util.BytesPrefix and
  First()/Last() instead of manual seek logic (AL-CT #1)
- Move TestDbUpgradeV6 to level_test.go with other upgrade tests
  (AL-CT #14)
marcopeereboom added a commit that referenced this pull request May 20, 2026
- Add comment documenting that inscription IDs use little-endian
  index per the ord protocol (AL-CT #5)
- Move ordinalindex_design.go to ordinalindex_design.md (AL-CT #11)
- Simplify OrdinalInscribedSatBounds using util.BytesPrefix and
  First()/Last() instead of manual seek logic (AL-CT #1)
- Move TestDbUpgradeV6 to level_test.go with other upgrade tests
  (AL-CT #14)
Add ordinal indexer to TBC that tracks individual satoshi
ownership via sat ranges and indexes Bitcoin inscriptions.
Follows the existing Indexer/indexer framework pattern,
modeled on zkindexer.go.

Database:
- New LevelDB "ordinals" with prefix-separated keys:
  'r' (sat ranges per UTXO), 'i' (inscription by ID),
  's' (inscribed sat → outpoint),
  'a' (sat → all inscriptions),
  'n' (inscriptions by block).
- OrdinalKey type, 7 new Database interface methods.
- Database version bump 5 → 6.

Indexer:
- FIFO sat range redistribution engine.
- Inscription envelope parser supporting all tags:
  content type, pointer, parent, delegate, metaprotocol.
- Cursed inscription detection (5 pre-jubilee rules).
- Flag-driven variable-length value encoding.
- Inscribed sat movement tracking via DB range scan.
- Zero dependency on txindex during wind/unwind.

Wiring:
- Config: OrdinalIndex bool, MaxCachedOrdinals int.
- Sync order: ui → ti → ki → zki → oi.
- Prometheus gauge, SyncInfo field, Synced() check.

Tests:
- 41 test functions covering FIFO engine, sat computation,
  envelope parsing, cursed detection, value encoding,
  pointer tag handling, key construction, cache scanning.
- Negative tests for malformed envelopes, boundary
  conditions, and edge cases.
- Updated TestDbUpgradeFull and TestDbUpgradeV5 for v6.
Implement the 6 ordinal RPCs defined in the SOW:
- InscriptionByID: lookup inscription by txid + input index
- InscriptionContent: retrieve raw content, follows delegation chains
- InscriptionsByBlock: enumerate inscriptions created in a block
- InscriptionsByAddress: cross-reference utxo index to find held inscriptions
- InscriptionsBySat: list all inscriptions on a sat (reinscription support)
- SatRangesByOutpoint: diagnostic lookup of sat ranges for a UTXO

Each RPC follows the existing dispatch pattern: Cmd constants and
request/response structs in api/tbcapi, dispatch cases in
getTBCAPICommandHandler, handler methods in rpc.go, and Server
business logic methods in tbc.go.

Adds decodeInscriptionValue (mirrors encodeInscriptionValue) with
round-trip tests including positive and negative cases.

Adds OrdinalInscriptionsBySat to the Database interface (iterates
the 'a' prefix for all inscription IDs on a given sat).
Wire OrdinalIndex to the TBC_ORDINAL_INDEX environment variable so
the ordinal indexer can be enabled at runtime. Follows the existing
TBC_HEMI_INDEX / TBC_ZK_INDEX pattern.
Performance fixes (measured before/after on testnet4 heavy zone):

1. Block-level inscribed-sat pre-scan: collect all input sat
   ranges per block, single DB scan for the merged range.
   Replaces per-input DB scans. O(inputs_per_block) → O(1).
   inscSat phase: 5m51s → 965ms (363x).

2. Sorted inscribed-sat slice with binary search: store block
   inscribed sats as sorted []uint64, binary search to find
   only sats overlapping each tx input range. Eliminates O(N)
   iteration over 96K+ inscribed sats per tx.
   Blocks taking 1.7s → <500ms. Zero SLOW blocks across full
   testnet4 sync.

3. Parallel fixupCacheHook: 128-way concurrent DB reads for
   input sat range pre-fetch. Mirrors fixupCacheChannel from
   utxoindex.go.

4. Min/max inscribed-sat boundary tracking: skip DB scan when
   block input range falls outside [minInscribedSat, maxSat].

5. OrdinalInscribedSatBounds: two LevelDB iterator seeks for
   min/max on restart. Replaces OrdinalInscribedSatsInRange(0,
   MaxUint64) which would load 70M entries (560MB) on mainnet.

Correctness fixes:

6. Fee sat conservation: two-pass windBlock processes non-coinbase
   txs first to collect fee ranges, then coinbase with subsidy +
   fees. Without this, fee sats vanish from the index.

7. Zero-value outputs: record empty sat ranges for txOut.Value==0
   so spendable UTXOs exist in the index. Root cause: testnet4
   block 32203 tx 73acc6ca...ff0e output 1 (value=0, v0_p2wpkh).

Adds benchmarks (ordinalindex_bench_test.go) validating each
optimization and design doc (ordinalindex_design.go) documenting
rationale with measured results.
TestOrdinalIndexFork exercises the ordinal indexer through
full wind/unwind cycles across three competing chains:

  Chain geometry:
             /-> b1a -> b2a (inscription A)
  genesis -> b1 -> b2 (inscription B) -> b3
             \-> b1b -> b2b (inscription C)

Exhaustive verification of all 5 ordinal entry types on every
wind/unwind transition:
  - n: InscriptionsByBlock (block sequence)
  - i: InscriptionByID (inscription data, txid, block hash)
  - s: OrdinalOutpointBySat (inscribed sat -> current outpoint)
  - a: InscriptionsBySat (sat -> inscription IDs)
  - r: SatRangesByOutpoint (UTXO sat ranges, zero-value outputs)

Precise sat number assertions verify FIFO split correctness:
  - [5B, 10B) splits to [5B, 8B) + [8B, 10B) + empty
  - create-and-spend in same block: [5B, 8B) -> [5B, 6.1B) + [6.1B, 8B)
  - coinbase subsidy: [10B, 15B) at height 2, [15B, 20B) at height 3
  - ranges verified on all 3 forks, confirmed absent after each unwind
  - inscribed sat tracks through tx chain (s entry at b3 tx2:0)

Fixes found by the test:
  - ordinalSatInscriptionKey value nil -> []byte{} (a prefix was
    silently deleted on every inscription)
  - batch-local annihilation: when a sat range is created and spent
    within the same cache batch, delete from cache instead of marking
    nil. Mirrors utxo indexer IsDelete/annihilation pattern via
    batchCreated set. Without this, unwind cannot restore ranges
    that were never persisted to DB.
parseEnvelopeTags did not recognize OP_0 (0x00) as the body
separator because envelopeTag() only matched OP_1..OP_16 and
single-byte data pushes. OP_0 returned -1, causing the body
content push to be silently skipped. Every InscriptionContent
call returned an empty body.

Add explicit OP_0 handling before the envelopeTag check. After
OP_0, all subsequent data pushes until OP_ENDIF or a recognized
tag are collected as body content. The existing case 5 (OP_5
body encoding) is preserved for backward compatibility.
Expose ordinal cache capacity as a configurable env var. Default
remains 1e6. Allows tuning for machines with less memory.
Two root causes of ordinal index data loss, fixed together:

1. OrdinalKey was type string, causing heap-allocated map keys.
Under heavy write load (~960K entries), GC pressure on
map[string][]byte corrupted goleveldb batch index during
Transaction.Write, triggering panic: "leveldb: invalid type".
Fix: OrdinalKey is now [45]byte, matching Outpoint and TxKey.
OrdinalValue type added with IsDelete()/Bytes() methods,
mirroring CacheOutput for the utxo indexer.

2. Cache entries silently lost between flush cycles. The
batchCreated annihilation pattern used delete(cache, key) to
remove map keys during processing. Combined with
fetchSatRangesParallel goroutines racing against cache.Clear()
on early return, this caused 10.57% of ordinal range entries
(1,488,292 / 14,079,206) to be missing from DB after a full
testnet4 index.
Fix: remove batchCreated entirely. Spent outpoints are marked
with nil (overwrite) instead of deleted from the map, matching
the utxo indexer sentinel pattern. Add defer w.Wait() in
fixupCacheHook and unwindBlock so goroutines always complete
before the cache can be cleared.

After: 0 missing ordinal range entries across 14,079,300 UTXOs.
Wire Is* methods (IsRange, IsSat, IsInscription, IsSatInscription,
IsBlockInscription) in existing TestKeyConstruction subtests, replacing
raw byte prefix checks with the typed methods.

Add TestOrdinalKeyIsMethods: table-driven cross-validation that each
key constructor activates exactly one Is* method. Includes zero-key
negative case.

Add TestOrdinalValueMethods: nil-is-delete sentinel, non-nil, and
empty-non-nil boundary cases.

Fix redundant txid copy in SatRangesByOutpoint — chainhash.Hash is
already [32]byte, no intermediate variable needed.
E2E tests (bitcoind + TBC + ordinal indexer + RPC websocket):

TestRpcOrdinalSatRanges: mines 10 blocks via bitcoind, syncs TBC
with ordinal indexing, queries coinbase sat ranges via websocket
RPC. Verifies FIFO sat allocation at height 1 and height 5 —
proves the full pipeline: bitcoind → P2P → TBC → ordinal indexer
→ LevelDB → RPC response.

TestRpcOrdinalNotFound: negative tests for all 5 ordinal RPC
endpoints (inscription by ID, inscription content, inscriptions
by block, inscriptions by sat, sat ranges by outpoint). Verifies
correct not-found/empty responses through the dispatch layer.

Helper: createTbcServerWithOrdinals creates a TBC server with
OrdinalIndex enabled and AutoIndex false for explicit sync
control via SyncIndexersToHash.

Unit tests:

FuzzParseInscriptionEnvelope: fuzz test for the inscription
parser. Seeds cover valid envelope, empty witness, truncated
data, wrong magic, and garbage. 45K+ executions without panic
in initial run.
TestRpcOrdinalInscriptionE2E proves the complete inscription
lifecycle through every component:

  gozer wallet → TBC RPC broadcast → bitcoind mempool →
  bitcoin block → TBC P2P sync → ordinal indexer → TBC RPC query

The test builds a real taproot inscription using the existing
wallet infrastructure:

1. Mine 101 blocks via bitcoind (mature coinbase)
2. gozer.UtxosByAddress finds a spendable P2PKH UTXO
3. Build commit TX: P2PKH spend → P2TR output with inscription
   script commitment (single-leaf tap tree, OP_TRUE + ord envelope)
4. wallet.TransactionSign signs the P2PKH input
5. gozer.BroadcastTx sends commit through TBC to bitcoind
6. Mine to confirm
7. Build reveal TX: script-path spend of committed P2TR with
   inscription witness [tapscript, controlBlock]
8. gozer.BroadcastTx sends reveal through TBC to bitcoind
9. Mine to confirm
10. TBC syncs, SyncIndexersToHash processes ordinals
11. Verify via websocket RPC: InscriptionByID, InscriptionContent
    (content type + body), SatRangesByOutpoint

No bitcoin-cli shortcuts for transaction handling — all TX
construction and signing goes through the gozer/wallet stack.
Adds 18 tests targeting every uncovered branch in the ordinal
inscription parser:

applyTag (25% → 100%): tag 2 pointer (valid + oversized), tag 3
parent (valid + invalid length), tag 7 metaprotocol, tag 11
delegate (valid + invalid length), default even/odd — all
exercised through the OP_0 body mid-tag path which is the only
call site for applyTag.

envelopeTag (80% → 100%): single-byte data push (OP_DATA_1)
tag encoding path.

parseEnvelopeTags (76.4% → 92.7%): tag 5 (OP_5 alternate content
encoding), OP_5 followed by recognized tag, body followed by tag,
tag value tokenizer exhaustion.

parseEnvelopeFromScript (86.7% → 93.3%): tokenizer exhaustion
after OP_FALSE, after OP_FALSE OP_IF.

Remaining gaps are indexer error paths in ordinalindex.go
(DB failures, context cancellation) that require mock injection.
TestRpcOrdinal exercises all 6 ordinal RPC handlers through the
websocket dispatch layer without requiring bitcoind. Seeds LevelDB
directly with ordinal entries via BlockOrdinalUpdate, starts TBC
with OrdinalIndex enabled, connects via websocket, and runs 7
table-driven subtests. Mirrors the TestRpcZK pattern.

Positive: SatRangesByOutpoint verified with pre-seeded range.
Negative: InscriptionByID, InscriptionContent, InscriptionsByBlock,
InscriptionsBySat, InscriptionsByAddress, SatRangesByOutpoint —
all not-found/empty paths exercised.

RPC handler coverage: 0% → 55-78%.
FuzzDecodeSatRanges: fuzz decode with round-trip verification
(decode → re-encode → decode must match). Skips non-multiple-of-16
inputs (panic by design). 20K+ executions clean.

FuzzDecodeInscriptionValue: fuzz decode with round-trip through
encodeInscriptionValue. Covers all flag combinations (cursed,
parent, delegate, metaprotocol). 60K+ executions clean.

FuzzDecodeVarUint: fuzz the little-endian variable-length uint
decoder. 76K+ executions clean.
decodeInscriptionValue: max uint64 sat round-trip, empty metaprotocol
(flag set, zero bytes remaining), unknown flag bits (4-7 set, decoder
ignores), exact 41-byte minimum, metaprotocol-only (no parent/delegate).

EncodeSatRanges/DecodeSatRanges: zero-count range round-trip,
max uint64 Start/Count round-trip.

decodeVarUint: max uint64 (8 × 0xff), overflow (>8 bytes silently
ignored via Go shift-past-64 semantics).
TestBlockOrdinalUpdateAndQuery: 16 subtests covering all 9 ordinal
DB functions with positive and negative paths. Seeds LevelDB
directly via BlockOrdinalUpdate, then queries each function.

Positive: OrdinalSatRangesByOutpoint, OrdinalInscriptionByID,
OrdinalInscriptionsByBlockHash, OrdinalInscriptionsBySat,
OrdinalOutpointBySat, OrdinalInscribedSatsInRange,
OrdinalInscribedSatBounds.

Negative: not-found for outpoint/inscription/sat lookups, empty
results for block/sat range queries, BlockHeaderByOrdinalIndex
without block headers.

BlockOrdinalUpdate unwind: verifies direction=-1 deletes entries.
TestBlockOrdinalUpdateEmptyCache: nil cache does not error.
TestRpcOrdinal: 13 table-driven subtests exercising all 6 ordinal
RPC handlers through the websocket dispatch layer. Seeds LevelDB
with ordinal + UTXO entries. Positive and negative paths for all
handlers including InscriptionsByAddress.

TestPrometheusOrdinalMetric: starts TBC with OrdinalIndex +
PrometheusListenAddress, curls /metrics, verifies
ordinal_sync_height gauge is present.

TestDbUpgradeV6: validates v5→v6 database upgrade (version bump,
data survives, ordinal DB functional after upgrade).

hemictl tbcdb: add ordinalrangesbyoutpoint, ordinalinscriptionbyid,
ordinalinscriptionsbyblock, ordinalinscriptionsbysat commands for
direct database inspection.
Wire ordinal read cache into satRanges and fetchSatRangesParallel.
Check LRU between write cache miss and DB read, populate on DB hit,
clear on sync complete via onSyncComplete hook.

Config: TBC_ORDINAL_READ_CACHE_SIZE (default "1gb").
Prometheus: ordinal_read_cache_{hits,misses,purges,size,items}.
Log line: rcache hits/usage percentages via readCacheInfo.

Fix BlockheaderCacheSize rename in test files.
Set ordinal indexer genesis to block 766854 (first inscription
767430 minus 576). Testnet3/4/localnet start from chain genesis.

Mainnet block hash is a placeholder — fill before mainnet use.
Remove all sat range precomputation from windBlock/windTx. The
ordinal indexer now only scans witness data for inscription
envelopes and stores reveals. No sat numbers, no FIFO
redistribution, no s-key tracking, no read cache, no fixup
pre-fetching.

Sat numbers and transfer tracking are deferred to query time
via backward walk to coinbase using raw blocks and tx index.

Remove ordinal read cache (config, prom, Server field) since
there are no DB reads during indexing.

Skip TestOrdinalIndexFork — expects s-key tracking which no
longer exists at index time.
Replace precomputed sat range DB lookups with on-demand computation
in SatRangesByOutpoint. Walks backward through the spending chain
via tx index and raw blocks to derive sat ranges at query time.

Coinbase outputs return subsidy-only ranges; fee sat computation
is deferred (requires resolving all block txs which is expensive).

Reduce hemictl cache allocations for CLI use (block cache 64mb,
disable utxo read cache).

Remove synthetic 'r'/'s'/'a' seed data from RPC tests. Skip
SatRangesByOutpoint, InscriptionsBySat, and InscriptionsByAddress
test cases that depend on removed precomputed data.
populateInscription now derives the inscribed sat number via
backward walk when the stored value is 0. Looks up the
inscription's input outpoint, computes sat ranges, and takes
the first sat.

This makes InscriptionByID and InscriptionsByBlock return
actual sat numbers instead of 0.

Removes OrdinalOutpointBySat lookup (current location tracking)
from populateInscription — transfer tracking is deferred.

Note: sat numbers are subsidy-only approximations until
coinbase fee computation is implemented.
Both RPCs depended on precomputed sat→inscription and
sat range lookups that were removed. Stub with TODO and
return empty results until outpoint→inscription tracking
is implemented at index time.
Replace full-output range computation in computeInscribedSat with
satTracer.traceSat: follows one sat through the FIFO using amounts
only (flat tx index lookups), always linear to coinbase.

At coinbase: if sat offset < subsidy, deterministic. If in fee
range, fee amounts identify which specific tx contributed the fee
sat, then follows that tx's input chain (same linear walk).

No fan-out, no resolving all block txs. Handles fees correctly
without the exponential blowup of the full-output approach.

Retain satRangeContext.compute for SatRangesByOutpoint RPC
(subsidy-only at coinbase — full output ranges are rarely queried).
Add TBC_REQUEST_TIMEOUT config option (default 10s, tbcd default
120s) replacing the hardcoded value. Longer timeouts accommodate
on-demand sat computation for deep ancestry chains.

Propagate context cancellation from computeInscribedSat through
populateInscription as an error instead of silently returning
sat_number=0. Add ctx.Err() check in the tracer loop to abort
promptly when the request context expires.
- Add comment documenting that inscription IDs use little-endian
  index per the ord protocol (AL-CT #5)
- Move ordinalindex_design.go to ordinalindex_design.md (AL-CT #11)
- Simplify OrdinalInscribedSatBounds using util.BytesPrefix and
  First()/Last() instead of manual seek logic (AL-CT #1)
- Move TestDbUpgradeV6 to level_test.go with other upgrade tests
  (AL-CT #14)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs This is a change to documentation area: hemictl This is a change to hemictl area: tbc This is a change to TBC (Tiny Bitcoin) changelog: done This pull request includes an appropriate update to CHANGELOG.md. size: XXL This change is extremely large (+/- 1000+). Changes this large should be split into multiple PRs type: feature This adds new functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants