Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
2b42b93
dag depth, markers, tests
bitcoin-coder-bob Feb 10, 2026
bed30a7
recursive CTE query for descendant markers, sweep test, prefetchVtxos…
bitcoin-coder-bob Feb 10, 2026
e8fbb86
testGetVtxoChainWithMarkerOptimization, linting
bitcoin-coder-bob Feb 10, 2026
e39e4d2
badger impl, migration file renames, marker_test.go
bitcoin-coder-bob Feb 10, 2026
94adccb
change vtxo table marker column to be JSONB to hold >=1 marker
bitcoin-coder-bob Feb 11, 2026
5c4ebc6
remove serivce.go markerStore nil checks, rearrange sql statements, lint
bitcoin-coder-bob Feb 12, 2026
7f613f1
swept column from vtxo table removed
bitcoin-coder-bob Feb 12, 2026
395e128
bulksweepmarkers, sweep all of a vtxo's markers
bitcoin-coder-bob Feb 12, 2026
cc93afb
bulk add new markers and vtxos
bitcoin-coder-bob Feb 12, 2026
9588705
new vtxos get markers set in AddVtxos
bitcoin-coder-bob Feb 12, 2026
18932e9
populate MarkerIds in getNewVtxosFromRound, createCheckpointSweepTask…
bitcoin-coder-bob Feb 12, 2026
56f6cd1
linted with go1.25.7
bitcoin-coder-bob Feb 12, 2026
c706f56
fix github action for linter version
bitcoin-coder-bob Feb 12, 2026
a88e895
github action linter to use latest golangci/golangci-lint-action@v8
bitcoin-coder-bob Feb 12, 2026
2a737f6
revert lint changes
bitcoin-coder-bob Feb 12, 2026
691480f
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Feb 12, 2026
bfa79c7
migration to ensure each vtxo has at least 1 marker, dust vtxos bulk …
bitcoin-coder-bob Feb 12, 2026
2fa7689
Tests
bitcoin-coder-bob Feb 12, 2026
bb6d7dc
more tests
bitcoin-coder-bob Feb 12, 2026
35c1944
20k depth test
bitcoin-coder-bob Feb 12, 2026
38a7945
more integration tests in service_test.go
bitcoin-coder-bob Feb 12, 2026
7d4d296
add marker for dust
bitcoin-coder-bob Feb 12, 2026
09f331d
safe copy, db tx usage, idx_marker_parent_markers index
bitcoin-coder-bob Feb 13, 2026
1122bd9
depthKnown, markersToMarshal fix
bitcoin-coder-bob Feb 13, 2026
bd5e93c
parentMarkerIds emply slice setting, test fix
bitcoin-coder-bob Feb 13, 2026
0046567
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Feb 16, 2026
307269e
lint fixes
bitcoin-coder-bob Feb 16, 2026
a6f0d8c
test optimizations, test comments
bitcoin-coder-bob Feb 17, 2026
fe8a755
sqlite json_each, badger touchups
bitcoin-coder-bob Feb 17, 2026
55618c4
cursor-based pagination for GetVtxoChain with lazy marker-window loading
bitcoin-coder-bob Feb 17, 2026
4dddd82
add end-to-end pagination tests and fix early termination bug
bitcoin-coder-bob Feb 17, 2026
49ed51c
Merge origin/master into bob/dag-1
bitcoin-coder-bob Feb 18, 2026
dc70231
make proto
bitcoin-coder-bob Feb 18, 2026
ae19758
Fix SweptAt timestamps, GetVtxosByArkTxid queries, and dust marker sweep
bitcoin-coder-bob Feb 23, 2026
d3de56e
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Feb 23, 2026
b7a8089
merge with master
bitcoin-coder-bob Feb 27, 2026
de7518f
merge with master to fix conflict in utils_test.go
bitcoin-coder-bob Feb 27, 2026
9463acb
make lint
bitcoin-coder-bob Mar 2, 2026
6905b83
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Mar 5, 2026
eabad6f
refactor marker logic into domain and carry depth via events
bitcoin-coder-bob Mar 16, 2026
678ca28
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Mar 18, 2026
fa0bf77
GetReadyUpdate return type fix
bitcoin-coder-bob Mar 18, 2026
3231064
log warning on marker window error
bitcoin-coder-bob Mar 19, 2026
86f9190
db migration to use milliseconds for swept_at
bitcoin-coder-bob Mar 19, 2026
ce2ef67
linting
bitcoin-coder-bob Mar 19, 2026
f13064c
optimize GetVtxoChain with marker-based bulk preloading (#973)
bitcoin-coder-bob Mar 20, 2026
2b8afb6
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Mar 20, 2026
f7cfde2
add benchmarks and weird-tree tests for GetVtxoChain
bitcoin-coder-bob Mar 23, 2026
8d8faba
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Apr 2, 2026
373eeec
lint
bitcoin-coder-bob Apr 2, 2026
453417e
use offchainTxCache to handle test reace condition
bitcoin-coder-bob Apr 2, 2026
69879d0
bulk-fetch offchain txs in walkVtxoChain to reduce DB round-trips (#1…
bitcoin-coder-bob Apr 15, 2026
40ff9f8
Merge branch 'master' into bob/dag-1
bitcoin-coder-bob Apr 16, 2026
7bb2a6a
fix badger fragile error matching
bitcoin-coder-bob Apr 16, 2026
a266382
fix marker sweep, SQLite atomicity, token TTL, and retry bounds
bitcoin-coder-bob Apr 16, 2026
b9f58d8
HMAC-sign pagination cursors to prevent auth bypass
bitcoin-coder-bob Apr 16, 2026
6833a6c
fix checkpoint sweep over-reach with per-outpoint tracking
bitcoin-coder-bob Apr 16, 2026
2ff4b78
fix benchmark metrics, SQL safety, and auth/marker edge cases
bitcoin-coder-bob Apr 17, 2026
30cc8c6
fix GetAllChildrenVtxos sibling over-reach and swept_vtxo down migrat…
bitcoin-coder-bob Apr 17, 2026
54924c0
fix hardcoded vout=0 when extracting leaf VTXO outpoints for sweep
bitcoin-coder-bob Apr 17, 2026
2d8c1ba
restore swept on rollback, fix isActive, document dual sweep path
bitcoin-coder-bob Apr 17, 2026
f6f3d65
Merge master into bob/dag-1
bitcoin-coder-bob Apr 27, 2026
e0d0c0e
Fix RepoManager interface stubs and test pointer dereference after ma…
bitcoin-coder-bob Apr 27, 2026
4bf8b90
dispatch batch update handler for non-ended rounds
bitcoin-coder-bob Apr 29, 2026
e8d55da
fix marker over-reach in batch sweep, add chain walk bound
bitcoin-coder-bob Apr 29, 2026
52ff7a2
drop WalletType from vtxo_chain_test, removed from InitArgs in master
bitcoin-coder-bob Apr 29, 2026
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
4 changes: 4 additions & 0 deletions api-spec/openapi/swagger/ark/v1/indexer.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,10 @@
"type": "integer",
"format": "int64"
},
"depth": {
"type": "integer",
"format": "uint32"
},
"expiresAt": {
"type": "integer",
"format": "int64"
Expand Down
4 changes: 4 additions & 0 deletions api-spec/openapi/swagger/ark/v1/service.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,10 @@
"type": "integer",
"format": "int64"
},
"depth": {
"type": "integer",
"format": "uint32"
},
"expiresAt": {
"type": "integer",
"format": "int64"
Expand Down
4 changes: 4 additions & 0 deletions api-spec/openapi/swagger/ark/v1/types.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@
"type": "integer",
"format": "int64"
},
"depth": {
"type": "integer",
"format": "uint32"
},
"expiresAt": {
"type": "integer",
"format": "int64"
Expand Down
1 change: 1 addition & 0 deletions api-spec/protobuf/ark/v1/indexer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ message IndexerVtxo {
repeated string commitment_txids = 11;
string settled_by = 12;
string ark_txid = 13;
uint32 depth = 14;
}

message IndexerChain {
Expand Down
1 change: 1 addition & 0 deletions api-spec/protobuf/ark/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ message Vtxo {
string spent_by = 11;
string settled_by = 12;
string ark_txid = 13;
uint32 depth = 14;
}

message TxData {
Expand Down
13 changes: 11 additions & 2 deletions api-spec/protobuf/gen/ark/v1/indexer.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions api-spec/protobuf/gen/ark/v1/indexer.pb.rgw.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions api-spec/protobuf/gen/ark/v1/types.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 90 additions & 1 deletion internal/core/application/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,11 @@ func (i *indexerService) GetVtxoChain(
nextVtxos := []domain.Outpoint{vtxoKey}
visited := make(map[string]bool)

// Pre-fetch VTXOs using markers for optimization (reduces DB calls for deep chains)
vtxoCache := i.prefetchVtxosByMarkers(ctx, vtxoKey)

for len(nextVtxos) > 0 {
Comment thread
louisinger marked this conversation as resolved.
vtxos, err := i.repoManager.Vtxos().GetVtxos(ctx, nextVtxos)
vtxos, err := i.getVtxosFromCacheOrDB(ctx, nextVtxos, vtxoCache)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -369,6 +372,92 @@ func (i *indexerService) GetVtxoChain(
}, nil
}

// prefetchVtxosByMarkers pre-fetches VTXOs using markers for optimization.
// This reduces the number of DB calls for deep chains by bulk fetching VTXOs
// associated with the marker chain instead of fetching one at a time.
func (i *indexerService) prefetchVtxosByMarkers(
ctx context.Context, startKey Outpoint,
) map[string]domain.Vtxo {
cache := make(map[string]domain.Vtxo)

if i.repoManager.Markers() == nil {
return cache
}

// Get starting VTXO to find its marker
startVtxos, err := i.repoManager.Vtxos().GetVtxos(ctx, []domain.Outpoint{startKey})
if err != nil || len(startVtxos) == 0 {
return cache
}

startVtxo := startVtxos[0]
// Add starting VTXO to cache
cache[startVtxo.Outpoint.String()] = startVtxo

if startVtxo.MarkerID == "" {
return cache
}

// Collect marker chain by following ParentMarkerIDs
markerIDs := []string{startVtxo.MarkerID}
marker, err := i.repoManager.Markers().GetMarker(ctx, startVtxo.MarkerID)
if err != nil {
return cache
}

// Follow the marker chain up to the root (depth 0)
for marker != nil && len(marker.ParentMarkerIDs) > 0 {
markerIDs = append(markerIDs, marker.ParentMarkerIDs...)
// Follow first parent marker to continue chain
marker, _ = i.repoManager.Markers().GetMarker(ctx, marker.ParentMarkerIDs[0])
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Bulk fetch VTXOs for all markers in the chain
vtxos, err := i.repoManager.Markers().GetVtxoChainByMarkers(ctx, markerIDs)
if err != nil {
return cache
}

for _, v := range vtxos {
cache[v.Outpoint.String()] = v
}

return cache
}

// getVtxosFromCacheOrDB retrieves VTXOs from cache first, falling back to DB for cache misses.
// This is used in conjunction with prefetchVtxosByMarkers to reduce DB calls.
func (i *indexerService) getVtxosFromCacheOrDB(
ctx context.Context,
outpoints []domain.Outpoint,
cache map[string]domain.Vtxo,
) ([]domain.Vtxo, error) {
result := make([]domain.Vtxo, 0, len(outpoints))
missingOutpoints := make([]domain.Outpoint, 0)

for _, op := range outpoints {
if v, ok := cache[op.String()]; ok {
result = append(result, v)
} else {
missingOutpoints = append(missingOutpoints, op)
}
}

if len(missingOutpoints) > 0 {
dbVtxos, err := i.repoManager.Vtxos().GetVtxos(ctx, missingOutpoints)
if err != nil {
return nil, err
}
result = append(result, dbVtxos...)
// Add to cache for future lookups in this chain traversal
for _, v := range dbVtxos {
cache[v.Outpoint.String()] = v
}
}

return result, nil
}

func (i *indexerService) GetVirtualTxs(
ctx context.Context, txids []string, page *Page,
) (*VirtualTxsResp, error) {
Expand Down
11 changes: 11 additions & 0 deletions internal/core/application/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,17 @@ func NewService(
return
}

// Calculate depth for new vtxos: max(parent depths) + 1
var maxDepth uint32
for _, v := range spentVtxos {
if v.Depth > maxDepth {
maxDepth = v.Depth
}
}
for i := range newVtxos {
newVtxos[i].Depth = maxDepth + 1
}

checkpointTxsByOutpoint := make(map[string]TxData)
for txid, tx := range offchainTx.CheckpointTxs {
// nolint
Expand Down
1 change: 1 addition & 0 deletions internal/core/application/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ func getNewVtxosFromRound(round *domain.Round) []domain.Vtxo {
RootCommitmentTxid: round.CommitmentTxid,
CreatedAt: createdAt,
ExpiresAt: expireAt,
Depth: 0, // new vtxo from batch starts at depth 0
})
}
}
Expand Down
31 changes: 31 additions & 0 deletions internal/core/domain/marker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package domain

// MarkerInterval is the depth interval at which markers are created.
// VTXOs at depth 0, 100, 200, etc. create new markers.
const MarkerInterval = 100
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what's happening to prod if we change this value ? like we have a DB with MarkerInterval 100 and we migrate to 150, do we need a specific migration for this ?

Copy link
Copy Markdown
Collaborator Author

@bitcoin-coder-bob bitcoin-coder-bob Mar 16, 2026

Choose a reason for hiding this comment

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

that would be problematic. newMarker would would create and skip at wrong intervals relative to the existing data, and parent marker linking would be inconsistent. I don't think we could have a migration to fix this, unless the migration is simply to rebuild the DAG if the constant is changed (pain). We need to treat this value as a "please do not change" constant. I could document that accordingly.


// Marker represents a DAG traversal checkpoint created at regular depth intervals.
// Markers enable compressed traversal of the VTXO chain by allowing jumps of
// MarkerInterval depths instead of traversing each VTXO individually.
type Marker struct {
// ID is the unique identifier for this marker (typically the VTXO outpoint)
ID string
// Depth is the chain depth at which this marker exists (0, 100, 200, ...)
Depth uint32
// ParentMarkerIDs is a list of marker IDs that this marker descends from
ParentMarkerIDs []string
}

// IsAtMarkerBoundary returns true if the given depth is at a marker boundary.
func IsAtMarkerBoundary(depth uint32) bool {
return depth%MarkerInterval == 0
}

// SweptMarker records when a marker (and all VTXOs it covers) was swept.
// This is an append-only table that enables efficient bulk sweep operations.
type SweptMarker struct {
// MarkerID is the ID of the marker that was swept
MarkerID string
// SweptAt is the Unix timestamp (milliseconds) when the marker was swept
SweptAt int64
}
44 changes: 44 additions & 0 deletions internal/core/domain/marker_repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package domain

import "context"

type MarkerRepository interface {
// AddMarker creates or updates a marker
AddMarker(ctx context.Context, marker Marker) error
// GetMarker retrieves a marker by ID
GetMarker(ctx context.Context, id string) (*Marker, error)
// GetMarkersByDepth retrieves all markers at a specific depth
GetMarkersByDepth(ctx context.Context, depth uint32) ([]Marker, error)
// GetMarkersByDepthRange retrieves all markers within a depth range
GetMarkersByDepthRange(ctx context.Context, minDepth, maxDepth uint32) ([]Marker, error)
// GetMarkersByIds retrieves markers by their IDs
GetMarkersByIds(ctx context.Context, ids []string) ([]Marker, error)

// SweepMarker marks a marker as swept at the given timestamp
SweepMarker(ctx context.Context, markerID string, sweptAt int64) error
// SweepMarkerWithDescendants marks a marker and all its descendants as swept
// Returns the number of markers swept (including descendants)
SweepMarkerWithDescendants(ctx context.Context, markerID string, sweptAt int64) (int64, error)
// IsMarkerSwept checks if a marker has been swept
IsMarkerSwept(ctx context.Context, markerID string) (bool, error)
// GetSweptMarkers retrieves swept marker records for the given marker IDs
GetSweptMarkers(ctx context.Context, markerIDs []string) ([]SweptMarker, error)

// UpdateVtxoMarker updates the marker_id for a VTXO
UpdateVtxoMarker(ctx context.Context, outpoint Outpoint, markerID string) error
// GetVtxosByMarker retrieves all VTXOs associated with a marker
GetVtxosByMarker(ctx context.Context, markerID string) ([]Vtxo, error)
// SweepVtxosByMarker marks all VTXOs with the given marker_id as swept
// Returns the number of VTXOs that were swept (not already swept)
SweepVtxosByMarker(ctx context.Context, markerID string) (int64, error)

// Chain traversal methods for GetVtxoChain optimization
// GetVtxosByDepthRange retrieves VTXOs within a depth range
GetVtxosByDepthRange(ctx context.Context, minDepth, maxDepth uint32) ([]Vtxo, error)
// GetVtxosByArkTxid retrieves VTXOs created by a specific ark tx
GetVtxosByArkTxid(ctx context.Context, arkTxid string) ([]Vtxo, error)
// GetVtxoChainByMarkers retrieves VTXOs that have markers in the given list
GetVtxoChainByMarkers(ctx context.Context, markerIDs []string) ([]Vtxo, error)

Close()
}
Loading
Loading