Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.regtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ services:
container_name: nbxplorer
ports:
- 32838:32838
image: nicolasdorier/nbxplorer:2.5.30
image: nicolasdorier/nbxplorer:2.5.30-1
environment:
- NBXPLORER_NETWORK=regtest
- NBXPLORER_CHAINS=btc
Expand Down
92 changes: 78 additions & 14 deletions internal/core/application/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,19 +569,20 @@ func (i *indexerService) walkVtxoChain(
chain := make([]ChainTx, 0)
nextVtxos := frontier
visited := make(map[string]bool)
offchainTxCache := make(map[string]*domain.OffchainTx)
allOutpoints := make([]Outpoint, 0)

// Lazy cache for VTXOs loaded during this page.
vtxoCache := make(map[string]domain.Vtxo)
loadedMarkers := make(map[string]bool)

// Eagerly preload VTXOs by walking the marker DAG upward.
// Eagerly preload VTXOs and offchain txs by walking the marker DAG upward.
if i.repoManager.Markers() != nil {
startVtxos, err := i.repoManager.Vtxos().GetVtxos(ctx, nextVtxos)
if err != nil {
return nil, nil, "", err
}
if err := i.preloadVtxosByMarkers(ctx, startVtxos, vtxoCache); err != nil {
if err := i.preloadByMarkers(ctx, startVtxos, vtxoCache, offchainTxCache); err != nil {
return nil, nil, "", err
}
}
Expand All @@ -601,6 +602,33 @@ func (i *indexerService) walkVtxoChain(
return nil, nil, "", fmt.Errorf("vtxo not found for outpoint: %v", nextVtxos)
}

missingOffchainTxids := make(map[string]struct{})
for _, vtxo := range vtxos {
if !vtxo.Preconfirmed {
continue
}
if _, ok := offchainTxCache[vtxo.Txid]; ok {
continue
}
missingOffchainTxids[vtxo.Txid] = struct{}{}
}

if len(missingOffchainTxids) > 0 {
txids := make([]string, 0, len(missingOffchainTxids))
for txid := range missingOffchainTxids {
txids = append(txids, txid)
}

offchainTxs, err := i.repoManager.OffchainTxs().GetOffchainTxsByTxids(ctx, txids)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to retrieve offchain txs: %s", err)
}

for _, tx := range offchainTxs {
offchainTxCache[tx.ArkTxid] = tx
}
}

newNextVtxos := make([]domain.Outpoint, 0)
for _, vtxo := range vtxos {
key := vtxo.Outpoint.String()
Expand Down Expand Up @@ -630,9 +658,14 @@ func (i *indexerService) walkVtxoChain(
// also, we have to populate the newNextVtxos with the checkpoints inputs
// in order to continue the chain in the next iteration
if vtxo.Preconfirmed {
offchainTx, err := i.repoManager.OffchainTxs().GetOffchainTx(ctx, vtxo.Txid)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to retrieve offchain tx: %s", err)
offchainTx, ok := offchainTxCache[vtxo.Txid]
if !ok {
var err error
offchainTx, err = i.repoManager.OffchainTxs().GetOffchainTx(ctx, vtxo.Txid)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to retrieve offchain tx: %s", err)
}
offchainTxCache[vtxo.Txid] = offchainTx
}

chainTx := ChainTx{
Expand Down Expand Up @@ -741,7 +774,6 @@ func (i *indexerService) walkVtxoChain(
nextVtxos = newNextVtxos
}

// Chain exhausted — no more pages.
return chain, allOutpoints, "", nil
}

Expand Down Expand Up @@ -775,20 +807,22 @@ func decodeChainCursor(token string) ([]domain.Outpoint, error) {
return outpoints, nil
}

// preloadVtxosByMarkers bulk-fetches VTXOs by walking the marker DAG upward
// from the markers of startVtxos. This reduces DB round-trips from O(chain_length)
// to O(chain_length / MarkerInterval).
func (i *indexerService) preloadVtxosByMarkers(
// preloadByMarkers bulk-fetches VTXOs and their offchain txs by walking the
// marker DAG upward from the markers of startVtxos. This reduces DB round-trips
// from O(chain_length) to O(chain_length / MarkerInterval) for both layers.
func (i *indexerService) preloadByMarkers(
ctx context.Context,
startVtxos []domain.Vtxo,
cache map[string]domain.Vtxo,
vtxoCache map[string]domain.Vtxo,
offchainTxCache map[string]*domain.OffchainTx,
) error {
markerRepo := i.repoManager.Markers()
offchainTxRepo := i.repoManager.OffchainTxs()

// Seed cache and collect initial marker IDs.
currentMarkerIDs := make(map[string]bool)
for _, v := range startVtxos {
cache[v.Outpoint.String()] = v
vtxoCache[v.Outpoint.String()] = v
for _, mid := range v.MarkerIDs {
currentMarkerIDs[mid] = true
}
Expand All @@ -809,8 +843,38 @@ func (i *indexerService) preloadVtxosByMarkers(
return err
}
for _, v := range vtxos {
if _, ok := cache[v.Outpoint.String()]; !ok {
cache[v.Outpoint.String()] = v
if _, ok := vtxoCache[v.Outpoint.String()]; !ok {
vtxoCache[v.Outpoint.String()] = v
}
}

// Piggyback: bulk-fetch the offchain txs for the preconfirmed VTXOs
// in this window, so the walk loop never has to hit the DB per-hop.
missingTxids := make([]string, 0, len(vtxos))
seen := make(map[string]bool, len(vtxos))
for _, v := range vtxos {
if !v.Preconfirmed {
continue
}
if seen[v.Txid] {
continue
}
seen[v.Txid] = true
if _, ok := offchainTxCache[v.Txid]; ok {
continue
}
missingTxids = append(missingTxids, v.Txid)
}
// offchainTxRepo may be nil in test helpers that do not wire up the
// offchain-tx repo. Skip the piggyback in that case — the walk loop
// will fall back to its own in-loop bulk fetch for any cache misses.
if len(missingTxids) > 0 && offchainTxRepo != nil {
offchainTxs, err := offchainTxRepo.GetOffchainTxsByTxids(ctx, missingTxids)
if err != nil {
return err
}
for _, tx := range offchainTxs {
offchainTxCache[tx.ArkTxid] = tx
}
}

Expand Down
Loading