Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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