domain,db: add VtxoKind discriminator for on-chain Arkade UTXOs (part of #1159) - #1161
bitcoin-coder-bob wants to merge 10 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. WalkthroughThe VTXO domain now distinguishes offchain and onchain kinds. PostgreSQL and SQLite persist and restore this discriminator. Onchain VTXOs are excluded from note classification and forfeiture checks, and never expire. Migration rollback tests cover both databases. ChangesVTXO kind support
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Feature Merge Risk: ⚪ Minimal · up to No merge-blocking issue is identified in the current change. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 13 files. (4 skipped: 4 unsupported.)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Adds a vtxo_kind column (0 = offchain, 1 = onchain) so an on-chain Arkade UTXO (issue #1159) can be told apart from a batch leaf or offchain-tx output. Without it, such a vtxo misclassifies: empty commitment fields read as a note, and the sweeper would treat SpentBy as a checkpoint txid. - domain.Vtxo gains a Kind field; IsNote() keys off it so an on-chain vtxo with empty commitments is not a note. - sqlite + postgres migrations add the column (DEFAULT 0 backfills all history as offchain) and recreate vtxo_vw / intent_with_inputs_vw so it is visible. badger persists it via gob with no migration; old records decode to offchain. - UpsertVtxo and rowToVtxo carry Kind on both SQL backends. Chosen as an explicit enum over a bool so future on-chain sub-kinds need no further migration. Behaviour-preserving: nothing writes Onchain yet, so every existing row is offchain and all classifiers evaluate as before. The protective read-guards (sweeper/indexer filters) land with the on-chain write path, where they are testable against real on-chain rows. Part of #1159.
rowToVtxo on both SQL backends already carried Kind, but the round-replay converter (combinedRowToVtxo) and the marker converters built domain.Vtxo values without it, so a vtxo read through those paths came back as VtxoKindOffchain regardless of its stored kind. Any guard keyed on Kind downstream of round replay or marker preload would have been silently wrong. Note combinedRowToVtxo also drops Depth and MarkerIDs, which predates this work. Left alone here rather than widening the change, but it means round replay does not reconstruct the DAG fields either.
An on-chain Arkade UTXO has no batch expiry, so ExpiresAt is not meaningful for it and is left zero. Without this guard IsExpired would compare against the Unix epoch and report every on-chain vtxo as expired, which cascades through RequiresForfeit and the two spend-path checks in service.go and would make them permanently unspendable. Behaviour-preserving today: nothing writes VtxoKindOnchain yet, so every existing row is offchain and evaluates exactly as before.
be8c7fc to
fc48791
Compare
|
This PR has been open for 3+ days without review. @bitcoin-coder-bob is anyone looking at this? |
|
This PR has been open for 14 days without review. @bitcoin-coder-bob is anyone looking at this? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha fc48791
Looks ready to merge.
The VtxoKind discriminator is a clean additive change. The two domain fixes it enables are necessary and correct:
IsNote(): An on-chain UTXO has no commitment txids, so without the kind check it would silently masquerade as a note. The earlyKind != VtxoKindOnchainguard is the right place.IsExpired(): A zeroExpiresAton an on-chain UTXO would read as the Unix epoch (1970), meaning every on-chain UTXO would appear permanently expired. The short-circuit is correct.
Migration safety: ALTER TABLE vtxo ADD COLUMN IF NOT EXISTS vtxo_kind INTEGER NOT NULL DEFAULT 0 — DEFAULT 0 means all existing rows map to VtxoKindOffchain with no backfill needed. The view recreation is straightforward.
The down.sql rolls back cleanly.
Tests cover both new cases. No concerns.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — domain,db: add VtxoKind discriminator (part of #1159)
What changed:
Adds (uint8 enum) to with (0, default) and . Fixes two methods:
-
**** — an on-chain Arkade UTXO has no commitment txids (like a note), so without the kind check it would read as a note. The guard prevents that.
-
**** — an on-chain Arkade UTXO has (zero value), which would always return true (1970 is in the past). The guard returns for , preventing all such vtxos from reading as permanently expired and being treated as unspendable.
** as open enum (uint8):** Intentional design note says future on-chain sub-kinds can be added without a schema migration — this means callers that switch on the kind need to handle unknown values. Worth ensuring any switch in downstream code has a default case.
Tests: Both and have the onchain fixture case. Good.
Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — VtxoKind discriminator
Verdict: looks ready to merge. Stacked on #1160.
Additive domain change to distinguish off-chain batch/offchain-tx VTXOs from on-chain Arkade UTXOs (#1159 feature).
Key correctness fixes this PR addresses:
IsNote(): on-chain Arkade UTXOs have no commitment txids, so without the kind check they'd read as notes. The guard is correct.IsExpired(): on-chain Arkade UTXOs carry a zeroExpiresAt(no batch expiry). Without the guard every such VTXO would be permanently "expired" and rejected by any check that callsIsExpired(). The guard is correct.
DB migration:
- Additive column:
ALTER TABLE vtxo ADD COLUMN IF NOT EXISTS vtxo_kind INTEGER NOT NULL DEFAULT 0. Existing rows getVtxoKindOffchain(0) automatically — no backfill needed. ✅ - Views dropped and recreated to expose the column. Down migration reverses this correctly. ✅
- sqlc-generated code updated consistently across all SELECT queries that project from
vtxo_vwandintent_with_inputs_vw. ✅
One observation: VtxoKind is typed as uint8 in Go but INTEGER (int32) in Postgres (via sqlc's sql.NullInt32/int32). The conversion domain.VtxoKind(row.VtxoKind) truncates int32 → uint8, which is fine as long as the column stays small (it will). A comment noting this would help future readers, but it's not blocking.
Tests are present and cover the two new cases (onchain kind not-a-note, onchain kind never-expired). ✅
|
This PR has been open for 5+ days without a review. @bitcoin-coder-bob is anyone looking at this? (VtxoKind discriminator (part of #1159); 5 days without review.) |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Summary
This PR adds a VtxoKind discriminator (VtxoKindOffchain=0, VtxoKindOnchain=1) to domain.Vtxo and wires it through SQLite, Postgres, and Badger persistence. The approach is sound for what it sets out to do, and the IsNote() / IsExpired() fixes are correct. However, there are several issues that need attention before merge.
🔴 Must-fix: RequiresForfeit() will misfire on VtxoKindOnchain vtxos
internal/core/domain/vtxo.go:81
func (v Vtxo) RequiresForfeit() bool {
return !v.Swept && !v.IsNote() && !v.Unrolled
}IsNote() now correctly returns false for VtxoKindOnchain. As a side-effect, an unspent, non-swept, non-unrolled on-chain vtxo will have RequiresForfeit() == true once #1159 begins writing them. This propagates to at least five call sites — some of which are not "sweeper/indexer" paths but round-processing and forfeit-initialization paths:
internal/core/application/service.go:1852— attempts taproot-script validation on the vtxo, which will either fail or pass on wrong script expectations.internal/infrastructure/live-store/inmemory/forfeits.go:37/redis/forfeits.go:51— adds the vtxo to the forfeit-signing list.internal/infrastructure/db/service.go:856— expects a matching forfeit txid inforfeitInputs; on-chain vtxos will silently map to an emptyspentBy.internal/core/domain/intent.go:117/round.go:174— gates round state transitions onRequiresForfeit().
The PR body says protective guards land with the write path (#1159). That is acceptable for the sweeper / SpentBy-as-checkpoint readers. But RequiresForfeit() is a domain-level invariant, not a sweeper concern; there is no call-site-level guard that prevents it from misfiring once on-chain vtxo records exist. A future PR writer or merge-conflict resolution can easily fail to add the guard before the write path is exercised.
Required action: Either extend RequiresForfeit() to return false for VtxoKindOnchain (the safest change here, symmetric with the IsNote() and IsExpired() fixes), or add an explicit TODO/compile-time note that every RequiresForfeit() call site must be guarded before #1159 lands. A test (TestVtxo_RequiresForfeit) covering the onchain-kind case would catch a regression.
🟡 Postgres down-migration has no automated roundtrip test
internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.down.sql
TestAddVtxoKindDownMigration (in vtxo_kind_down_test.go) runs against an in-memory SQLite instance via newSweptVtxoMigrator. The Postgres down migration is untested at the migration-layer level; the integration run described in the PR ("fresh DB") exercises the up path only. If the Postgres down migration has a syntax or dependency-ordering error it will surface only in a live rollback.
This is lower risk than the RequiresForfeit issue but should be noted. Consider a CI job or separate test that applies the full Postgres migration chain and back.
🟡 combinedRowToVtxo still drops Depth and MarkerIDs
internal/infrastructure/db/postgres/round_repo.go:708 / internal/infrastructure/db/sqlite/round_repo.go:849
The PR correctly flags this pre-existing gap. After this change, Kind is now wired through round replay but Depth and MarkerIDs are still dropped. Any downstream code that branches on Kind for vtxos read through round replay will have a vtxo with correct Kind but incorrect Depth and no MarkerIDs. That mismatch could produce silent errors if DAG-aware logic is added before combinedRowToVtxo is fixed. A follow-up issue / TODO comment in the code would help.
🟡 No range-check on VtxoKind at deserialization
internal/infrastructure/db/postgres/vtxo_repo.go:648, internal/infrastructure/db/sqlite/vtxo_repo.go:737, and all combinedRowToVtxo sites.
Kind: domain.VtxoKind(row.VtxoKind),VtxoKind is declared as uint8; the stored column is INTEGER (32/64-bit). An out-of-range value stored by a future migration, a direct SQL edit, or a bug in #1159 will cast silently to an undefined VtxoKind value, bypassing all IsNote() / IsExpired() guards. Consider a small helper:
func vtxoKindFromInt(v int32) (VtxoKind, error) {
switch VtxoKind(v) {
case VtxoKindOffchain, VtxoKindOnchain:
return VtxoKind(v), nil
default:
return VtxoKindOffchain, fmt.Errorf("unknown vtxo_kind %d", v)
}
}Or at minimum a metric / log on unrecognised values so the operator knows before a customer reports funds gone.
🟡 SQLite down migration: DROP COLUMN without IF EXISTS
internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.down.sql:3
ALTER TABLE vtxo DROP COLUMN vtxo_kind;The Postgres down migration uses DROP COLUMN IF EXISTS. The SQLite variant does not. If the column is missing (interrupted partial rollback), the SQLite down migration will error out rather than succeed idempotently. On SQLite ≥ 3.35.0 IF EXISTS is supported for DROP COLUMN; prefer consistency with Postgres.
✅ Confirmed correct
DEFAULT 0backfill: existing rows correctly becomeVtxoKindOffchainwith no data migration.vtxo_vwisSELECT v.*and picks upvtxo_kindautomatically after recreation — no column enumeration risk.intent_with_inputs_vwenumerates columns and hasv.vtxo_kindadded explicitly in the up migration — correct.- Badger
vtxoDTOembedsdomain.Vtxovia gob; old records decode withKind=0(offchain) — correct. IsNote()andIsExpired()fixes are well-reasoned and tested.- View drop order (intent first, then vtxo_vw) is safe —
intent_with_inputs_vwselects from the basevtxotable, not fromvtxo_vw. combinedRowToVtxoKindwiring: Postgres usessql.NullInt32.Int32, SQLite usessql.NullInt64.Int64— consistent with the respective model types.- No cross-repo API surface change detected:
VtxoKindis internal toarkd; SDKs (ts, go, rust, dotnet) have novtxo_kindreferences.
Noted / out of scope (acknowledged in PR body)
SelectSweepableUnrolledVtxoswill surface on-chain vtxos if they ever haveunrolled=true; sweeper guard deferred to write path — acceptable for now.SpentBy-as-checkpoint reader routing for on-chain vtxos — same deferral, same reasoning.
An on-chain Arkade UTXO joins a batch as a boarding input, signed directly and never forfeited. IsNote already excludes the kind, which flipped RequiresForfeit to true for it, so guard it explicitly alongside IsNote and IsExpired.
Same up, down, re-apply sequence on both SQL backends, against a database the test owns on the shared test server.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Follow-up pass — 9c37440 vs b1e0e7b. Two substantive commits. Zero prior issues to close (previous pass at fc48791 raised none).
Prior issues
None raised in the last pass; nothing to close.
New commits in scope
Three commits land since b1e0e7b:
| sha | subject |
|---|---|
f863e48 |
Migrate to btcd v0.26 and the v2 sub-modules (#1173) — merged from master, reviewed separately |
4b3a433 |
domain: on-chain kind never requires a forfeit |
262e626 |
db: run the add_vtxo_kind down-migration test on postgres too |
The btcd v0.26 rename (f863e48) is a mechanical import-path rewrite that arrived via the master merge; it does not change any logic on this branch. I will not re-audit it here.
4b3a433 — RequiresForfeit() guard (internal/core/domain/vtxo.go:96)
Correct. Before this commit, RequiresForfeit() was:
return !v.Swept && !v.IsNote() && !v.UnrolledFor a VtxoKindOnchain VTXO that carries commitment txids (so IsNote() correctly returns false because of the kind check already there), the old expression evaluates true — signalling a forfeit path that onchain boarding inputs never take. The new guard:
return v.Kind != VtxoKindOnchain && !v.Swept && !v.IsNote() && !v.Unrolledshort-circuits immediately for any onchain kind, regardless of sweep/note/unroll state.
The added test case (vtxo_test.go:191-202) exercises exactly the dangerous scenario: an onchain VTXO with a non-empty CommitmentTxids slice (which would have defeated the IsNote() escape) and a future ExpiresAt, asserting requiresForfeit: false. Coverage is complete.
No concerns.
262e626 — Postgres down-migration test (internal/infrastructure/db/vtxo_kind_down_test.go)
Clean. The refactor extracts a schema interface and adds a postgresSchema implementation, so the same up/down/re-up assertion sequence runs on both backends.
Points checked:
newVtxoKindPostgresMigratorcallsnewMigrator().Drop()to wipe prior state, then constructs a fresh instance — correct, becauseDropinvalidates the migrator it ran on.information_schema.columnsquery is fully parameterised ($1,$2); no injection surface.- The SQLite
PRAGMA table_info(%s)path remains unparameterised, but table names are hardcoded literals in the test — no risk. - The dedicated database (
vtxo_kind_migration) is separate fromTestService's database, so the two cannot clobber each other. - Hard-coded test-server credentials in a test constant is the existing project pattern; no new exposure.
One observation worth a follow-up (not a blocker): t.Run("postgres", ...) will hard-fail on any machine without a reachable test postgres server. The SQLite sub-test will still pass in isolation. If there are developer environments without postgres, a testing.Short() skip or a build tag would protect them. This matches the existing TestService behaviour, so it is not a regression here.
Summary
All new logic is correct. The RequiresForfeit fix closes a real gap: without it, any onchain VTXO that carried commitment txids would be treated as requiring a forfeit signature it can never provide. The migration-test extension gives both backends the same rollback coverage. No blocking issues.
|
Changes were requested on this PR (open 40+ days). @bitcoin-coder-bob need any help addressing the feedback? (VtxoKind discriminator) |
Addressed on 9c37440: RequiresForfeit guards the on-chain kind with a test, and the down-migration test runs on postgres too. The follow-up review on 2026-09-02 confirmed both. Dismissing the stale request.
|
This PR has been open for 41+ days without a review. @bitcoin-coder-bob — VtxoKind discriminator: is this waiting on other PRs in the series to land first? |
|
This PR has been open for 41 days without review. @bitcoin-coder-bob is the VtxoKind discriminator still moving forward? |
An on-chain Arkade UTXO has no batch behind it and so no expiry, and stores a zero rather than a timestamp that would not be true. #1161 stopped IsExpired reading that zero as 1970 and marking the vtxo permanently expired. Three places read the field directly rather than through the predicate, and this covers them. The one that bites is the minimum-expiry loop in SubmitOffchainTx. It walks the spent vtxos for the soonest expiry, starting from MaxInt64, and carries the matching root commitment txid along with it. A zero is smaller than any real deadline, so a single on-chain input would win outright, date the resulting vtxo to the epoch, and take the wrong commitment txid with it. Extracted as earliestBatchExpiry so the rule is testable rather than buried in a long method. The fee manager is the second. OffchainInput already treats the zero time as "no expiry" and omits the CEL variable, but converting a zero ExpiresAt with time.Unix produces 1970, which is not zero and hands the fee expression a deadline in the past. It now passes the zero time through, so the expression sees no expiry at all. The settlement expiry gap is the third and is deliberately no more than intent: a zero already fails that comparison, so the early return changes no outcome today. It is there so reversing the comparison later cannot silently start rejecting on-chain vtxos, and its comment and test both say so rather than claiming a fix. HasBatchExpiry names the condition once instead of repeating a kind check across three packages.
|
This PR (domain,db: add VtxoKind discriminator for on-chain Arkade UTXOs) has been open for 3+ days without review. @bitcoin-coder-bob is anyone looking at this? |
The VtxoKind doc comment carried an issue number, which belongs in the commit message and the PR description rather than in the source. Two comments in the down-migration test used a colon where a sentence reads better, one of them as a bare Up label whose Down counterpart was already written as prose. The test's support constants sat above the test. Moved to the bottom with the rest of the scaffolding, so the file opens on the test.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Follow-up pass — 1 cosmetic commit since 9c37440, 0 prior findings, 0 remain.
Prior findings
My previous pass (sha fc48791a) raised no issues. Nothing to close out.
Incremental diff — commit 3bf9080
Single commit: "domain,db: drop an issue reference and tidy the migration test layout."
Two files changed, purely cosmetic:
internal/core/domain/vtxo.go (+2/−2)
Removed the inline issue reference (issue #1159) from the VtxoKind doc comment and reflowed the sentence. No behavioural change.
internal/infrastructure/db/vtxo_kind_down_test.go (+11/−11)
- Moved the
constblock (addVtxoKindMigrationVersion,addVtxoKindPostgresDsn) from the top of the file to the bottom alongside the rest of the test scaffolding. - Rephrased two comments from colon-label style to sentence prose.
No logic, schema, protocol, or interface changes in this commit. No new findings.
Summary
This commit is housekeeping only. All protocol logic reviewed in the previous pass is unchanged. No new issues.
…tions The audits that produced the earlier comment cleanups only ever looked at Go files, so the SQL comments in both backends kept an issue number and two colon sentences. Same wording in postgres and sqlite, since the pair should stay readable side by side.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Follow-up pass — 3bf9080…54adb21. One cosmetic commit (SQL comment rewording only). No functional change.
Prior issues from the substantive PROTOCOL-CRITICAL review (the one now marked dismissed): 3 of 5 resolved, 2 still open.
Prior-issue closure
1. 🔴 RequiresForfeit() misfires on VtxoKindOnchain — Fixed
internal/core/domain/vtxo.go: The guard v.Kind != VtxoKindOnchain && was added before the existing !v.Swept && !v.IsNote() && !v.Unrolled expression. TestVtxo_RequiresForfeit covers the onchain-kind case. Resolved.
2. 🟡 Postgres down-migration has no automated roundtrip test — Fixed
internal/infrastructure/db/vtxo_kind_down_test.go: TestAddVtxoKindDownMigration now has both t.Run("sqlite", …) and t.Run("postgres", …) sub-tests, with newVtxoKindPostgresMigrator exercising the full apply/rollback/reapply cycle on a real Postgres instance. Resolved.
3. 🟡 combinedRowToVtxo did not carry Kind — Fixed
internal/infrastructure/db/postgres/round_repo.go:727 and internal/infrastructure/db/sqlite/round_repo.go:869: Both now assign Kind: domain.VtxoKind(row.VtxoKind.Int32) / .Int64. The pre-existing Depth / MarkerIDs gap is acknowledged in the PR and unchanged by this PR — still open as a tracked pre-existing gap, not this PR's subject.
4. 🟡 No range-check on VtxoKind at deserialization — Still open
Every rowToVtxo path in both backends casts directly:
// postgres vtxo_repo.go, round_repo.go, marker_repo.go (multiple sites)
Kind: domain.VtxoKind(row.VtxoKind.Int32)
// sqlite vtxo_repo.go, round_repo.go, marker_repo.go (multiple sites)
Kind: domain.VtxoKind(row.VtxoKind.Int64)An out-of-range integer stored by a future migration or direct SQL edit silently becomes an undefined VtxoKind, bypassing all IsNote() / IsExpired() / RequiresForfeit() guards. The new commit does not touch this. A minimal guard (switch or range check returning an error or defaulting to VtxoKindOffchain + logging) would eliminate the silent misclassification risk.
5. 🟡 SQLite down migration: DROP COLUMN without IF EXISTS — Still open
internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.down.sql:7:
ALTER TABLE vtxo DROP COLUMN vtxo_kind;The Postgres counterpart uses DROP COLUMN IF EXISTS vtxo_kind. SQLite ≥ 3.35.0 supports IF EXISTS on DROP COLUMN; omitting it makes the SQLite down migration non-idempotent. If a rollback is interrupted after the column is dropped but before the migration version is updated, re-running the down migration fails instead of recovering gracefully. The new commit only rewrites the opening comment; the DDL is unchanged.
New commit (54adb21)
SQL comments only — four migration files, two lines each, reformatting from colon-separated clauses to prose sentences. Zero functional change. No new findings.
Summary
The one blocking issue (RequiresForfeit() misfire) and the two actionable 🟡 items (Postgres roundtrip test, combinedRowToVtxo Kind wiring) are all resolved. The two remaining 🟡 items (no deserialization range-check, SQLite DROP COLUMN non-idempotence) are unchanged from the prior pass and neither is addressed by the new commit. Both are low-severity but worth tracking before the on-chain write path (#1159) goes live.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — arkade-os/arkd #1161
domain,db: add VtxoKind discriminator for on-chain Arkade UTXOs (part of #1159)
Summary
Foundation slice for the on-chain Arkade UTXO feature. Adds the VtxoKind discriminator to the Vtxo domain struct and updates the three predicates that otherwise misclassify on-chain UTXOs.
What's correct
IsNote(): The kind check prevents an on-chain UTXO (which has no commitment txids) from being read as a note — correct.RequiresForfeit(): On-chain UTXOs join a batch as boarding inputs, never as forfeitable vtxos — correct.IsExpired(): An on-chain UTXO carriesExpiresAt == 0(no batch), which without the guard would read as permanently expired — the guard is correct.- Postgres migration:
vtxo_kindcolumn added with a default of0(=VtxoKindOffchain), so existing rows are correctly classified without backfill. The views are rebuilt to expose the column. Down migration is included. rowToVtxo*functions: Both scan paths updated to populateKindfrom the DB row.- Test coverage:
TestVtxo_IsNote,TestVtxo_IsExpired, andTestVtxo_RequiresForfeiteach get a new fixture for the on-chain kind.
Minor question
The Badger and SQLite backends aren't shown in this diff — do they also persist VtxoKind, or is it only stored in Postgres? If the other backends use a different serialisation format they'd also need updating before on-chain UTXOs are written through them.
Verdict
Looks ready as a foundation slice. Confirm backend coverage before merging the slices that write VtxoKindOnchain records. Human review required due to schema change and VTXO domain impact.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — VtxoKind discriminator for on-chain Arkade UTXOs
Verdict: looks correct and well-structured; protocol-critical schema change — please have a human reviewer confirm before merging.
What this does
Adds a Kind VtxoKind discriminator field to the Vtxo domain type, with two values: VtxoKindOffchain (0, default — existing behaviour) and VtxoKindOnchain (1). Existing rows default to 0 via the migration's DEFAULT 0, so no backfill is needed. Fixes three predicate methods that previously gave wrong answers for on-chain vtxos:
IsNote()— an on-chain vtxo has no commitment txids and would have been misread as a note.RequiresForfeit()— an on-chain vtxo joins as a boarding input, never a forfeit.IsExpired()— a zeroExpiresAton an on-chain vtxo would have read as permanently expired.
Assessment
- The DB migration (
20260901000000_add_vtxo_kind) adds the column withNOT NULL DEFAULT 0and reconstructs both views. The down migration is also present and consistent. - The open-enum comment explicitly reserves space for future sub-kinds without schema migrations, which is the right design given how PostgreSQL handles integer comparisons.
- Test coverage covers all three fixed predicates for the on-chain case.
- The
rowToVtxoFromVtxoVwandrowToVtxoFromMarkerQueryfunctions in the Postgres layer now populateKind. Check that the Badger and in-memory backends are also updated — not visible in this diff slice.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Flagged for human review — introduces a new VTXO kind that affects protocol-level classification.
This is the foundational PR for the VtxoKindOnchain feature. Key observations:
VtxoKindis defined asuint8withiota, defaulting toVtxoKindOffchain = 0. Existing vtxos default to 0 in both the postgres and sqlite migrations (NOT NULL DEFAULT 0), so no backfill is needed and rollback is safe.IsNote(),RequiresForfeit(), andIsExpired()are all correctly guarded forVtxoKindOnchain. TheIsExpiredguard is especially important: without it, a zeroExpiresAtwould read as permanently expired.- Both postgres and sqlite migrations are present and symmetric. The down migrations reconstruct views without
vtxo_kind. The postgresDROP VIEW IF EXISTSordering is correct. - Generated SQLC code (
models.go,query.sql.go) is updated consistently across both backends. rowToVtxoFromVtxoVw,rowToVtxoFromMarkerQuery, andcombinedRowToVtxoinround_repo.goall correctly populateKind.
Minor note: VtxoKind is described as an open enum but the type is uint8. Future sub-kinds would simply occupy additional iota values. Worth a quick grep to confirm no exhaustive switch (without default) would silently mis-classify future kinds — none were found in this diff.
No security issues found. Looks ready to merge after human sign-off.
Second slice of #1159 (on-chain Arkade execution). Not stacked on #1160: it shares no files with #1160 or #1162, is based on master, and merges cleanly on its own. See "Merge ordering" below for why it should still go in first.
Adds the
VtxoKinddiscriminator. The design doc's original "no schema change" assumption was false. A purely on-chain-derived Arkade UTXO misclassifies against the existing vtxo fields (empty commitment fields read as a note viaIsNote();Unrolled=trueroutes into the sweeper which treatsSpentByas a checkpoint txid). A discriminator resolves this.Confirmed on the #1159 design call: the direction is a unified indexer with a type flag rather than a separate on-chain indexer, which is what this builds. An
on-chain unconfirmedstate is wanted eventually, which the enum absorbs without a second migration (see "Why an enum, not a bool" below).What this does
domain.Vtxogains aKindfield (VtxoKindOffchain=0,VtxoKindOnchain=1).IsNote()keys off it so an on-chain vtxo with empty commitments is not a note, andIsExpired()returns false for it since there is no batch expiry on-chain.RequiresForfeit()returns false for the on-chain kind. Excluding it fromIsNote()had flipped this to true, and an on-chain UTXO joins a batch as a boarding input that is signed directly and never forfeited. Guarded explicitly alongsideIsNote()andIsExpired().vtxo_kind INTEGER NOT NULL DEFAULT 0and recreatevtxo_vw/intent_with_inputs_vwso it is visible (vtxo_vwisSELECT v.*; the intent view enumerates columns, sov.vtxo_kindis added by hand).DEFAULT 0backfills all history as offchain, no data migration.UpsertVtxo(write) androwToVtxo(read) carryKindon both SQL backends.Why an enum, not a bool
Same migration cost, but future on-chain sub-kinds (assets, cosigned-vs-unilateral-exit) need no further migration. This was a judged decision (explicit enum vs bool vs derive-from-fields); derive-from-fields was rejected because it overloads
Preconfirmedfor every reader and opens a new fraud misfire.Scope / behaviour
Behaviour-preserving: nothing writes
Onchainyet, so every existing row is offchain and all classifiers evaluate as before. The protective read-guards (sweeper/indexer filters that keep on-chain rows away from theSpentBy-as-checkpoint readers) deliberately land with the on-chain write path, where they are testable against real on-chain rows.Second commit: carry Kind through the round and marker converters
rowToVtxocarriedKindfrom the start, but the round-replay converter (combinedRowToVtxo) and the marker converters builtdomain.Vtxovalues without it, so a vtxo read through those paths came back asVtxoKindOffchainno matter what was stored. Any guard keyed onKinddownstream of round replay or marker preload would have been silently wrong, which is the kind of gap that only shows up once something depends on it. Now wired on both SQL backends.Adjacent finding, left alone:
combinedRowToVtxoalso dropsDepthandMarkerIDs. That predates this PR (it came in with the DAG work), so round replay does not reconstruct the DAG fields either. Flagged rather than fixed here to keep this change scoped.Test plan
go build ./...,make lint(0 issues), gofmt cleanTestVtxo_IsNoteextended: an onchain-kind vtxo with empty commitments is not a noteTestVtxo_RequiresForfeitgains the on-chain-kind case, verified to fail without the guardTestAddVtxoKindDownMigration(new) runs on both sqlite and postgres: up adds the column and surfaces it invtxo_vw, down drops it and recreates the views, re-apply works. The postgres case runs against a database the test owns on the shared test server, so it never touchesTestServicestateTestService/.../postgres_stores) run on a fresh DB, migration chain applies and the vtxo round-trip carriesKindMerge ordering
Merge this one first of the open #1159 PRs (#1160, #1161, #1162, and #1174, which also waits on this: its onchain-spend tracking is scoped to
unrolled = truetoday, and the follow-up that widens it toKind = Onchainbuilds on the discriminator added here). Not for a code dependency, there is none and the three merge cleanly in any order, but because of the migration. It is stamped20260901000000, and the repo uses golang-migrate, which tracks a single current version and silently skips any migration numbered below it. If a later-stamped migration lands on master and reaches a deployed database before this merges, thevtxo_kindcolumn never gets added there. Master's latest migration is20260807120000today. If that changes before this merges, bump this migration's timestamp past it.Review notes
DROP COLUMN IF EXISTS(syntax error, checked on 3.37.2), so the sqlite down migration drops the column unconditionally while the postgres one keepsIF EXISTS. That asymmetry is deliberate.combinedRowToVtxostill dropsDepthandMarkerIDs. Pre-existing, tracked under "Known gaps in reused code" in On-chain Arkade execution: arkd as an on-chain co-signing oracle #1159.Status
Out of draft. The discriminator representation (unified indexer with a type flag, enum rather than bool) was confirmed on the #1159 design call.
Summary by CodeRabbit
New Features
Database
Tests