Skip to content

domain,db: add VtxoKind discriminator for on-chain Arkade UTXOs (part of #1159) - #1161

Open
bitcoin-coder-bob wants to merge 8 commits into
masterfrom
bob/onchain-arkade-vtxo-kind
Open

domain,db: add VtxoKind discriminator for on-chain Arkade UTXOs (part of #1159)#1161
bitcoin-coder-bob wants to merge 8 commits into
masterfrom
bob/onchain-arkade-vtxo-kind

Conversation

@bitcoin-coder-bob

@bitcoin-coder-bob bitcoin-coder-bob commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

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 VtxoKind discriminator. 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 via IsNote(); Unrolled=true routes into the sweeper which treats SpentBy as 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 unconfirmed state is wanted eventually, which the enum absorbs without a second migration (see "Why an enum, not a bool" below).

What this does

  • domain.Vtxo gains a Kind field (VtxoKindOffchain=0, VtxoKindOnchain=1). IsNote() keys off it so an on-chain vtxo with empty commitments is not a note, and IsExpired() returns false for it since there is no batch expiry on-chain.
  • RequiresForfeit() returns false for the on-chain kind. Excluding it from IsNote() 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 alongside IsNote() and IsExpired().
  • sqlite + postgres migrations add vtxo_kind INTEGER NOT NULL DEFAULT 0 and recreate vtxo_vw / intent_with_inputs_vw so it is visible (vtxo_vw is SELECT v.*; the intent view enumerates columns, so v.vtxo_kind is added by hand). DEFAULT 0 backfills all history as offchain, no data migration.
  • badger persists it via gob with no migration; old records decode to the zero value (offchain).
  • UpsertVtxo (write) and rowToVtxo (read) carry Kind on 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 Preconfirmed for every reader and opens a new fraud misfire.

Scope / behaviour

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 that keep on-chain rows away from the SpentBy-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

rowToVtxo carried Kind from the start, 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 no matter what was stored. Any guard keyed on Kind downstream 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: combinedRowToVtxo also drops Depth and MarkerIDs. 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 clean
  • TestVtxo_IsNote extended: an onchain-kind vtxo with empty commitments is not a note
  • TestVtxo_RequiresForfeit gains the on-chain-kind case, verified to fail without the guard
  • TestAddVtxoKindDownMigration (new) runs on both sqlite and postgres: up adds the column and surfaces it in vtxo_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 touches TestService state
  • sqlite package suite green
  • postgres full integration (TestService/.../postgres_stores) run on a fresh DB, migration chain applies and the vtxo round-trip carries Kind
  • badger + core + domain suites green

Merge 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 = true today, and the follow-up that widens it to Kind = Onchain builds 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 stamped 20260901000000, 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, the vtxo_kind column never gets added there. Master's latest migration is 20260807120000 today. If that changes before this merges, bump this migration's timestamp past it.

Review notes

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

    • VTXOs now distinguish between off-chain and on-chain holdings.
    • On-chain VTXOs are excluded from note classification, do not require forfeits, and do not expire.
    • VTXO type is preserved when storing and retrieving data.
  • Database

    • Existing VTXOs default to off-chain during migration.
    • Database migrations can be safely applied and reversed.
  • Tests

    • Added coverage for on-chain behavior, persistence, and migration reversibility.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 262d4996-6ab1-4b6a-9ebb-eb697772b25b

📥 Commits

Reviewing files that changed from the base of the PR and between b1e0e7b and 9c37440.

📒 Files selected for processing (3)
  • internal/core/domain/vtxo.go
  • internal/core/domain/vtxo_test.go
  • internal/infrastructure/db/vtxo_kind_down_test.go

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

The VTXO domain now distinguishes offchain and onchain kinds. PostgreSQL and SQLite migrations, queries, repositories, and row converters persist and restore this discriminator. Onchain VTXOs are excluded from note classification and forfeiture checks, and never expire. Migration rollback coverage now includes both databases.

Changes

VTXO kind support

Layer / File(s) Summary
Domain kind semantics
internal/core/domain/vtxo.go, internal/core/domain/vtxo_test.go
Adds VtxoKind, stores it on Vtxo, applies onchain-specific note, forfeiture, and expiry rules, and tests these cases.
PostgreSQL kind persistence
internal/infrastructure/db/postgres/...
Adds vtxo_kind to the PostgreSQL schema and views. Generated queries persist and select it. Repository converters map it to domain.Vtxo.Kind.
SQLite kind persistence
internal/infrastructure/db/sqlite/...
Adds vtxo_kind to the SQLite schema and views. Generated queries and repositories persist and restore it.
Cross-backend migration validation
internal/infrastructure/db/vtxo_kind_down_test.go
Applies, rolls back, and reapplies the migration on SQLite and PostgreSQL while checking the table column and views.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: 🔵 Low · up to 9c374

The change adds a discriminator that correctly separates on-chain and off-chain lifecycle behavior, but rolling back the database migration after on-chain records exist can erase that distinction and restore incorrect handling. The PR is mergeable with explicit owner awareness and follow-up to make rollback value-preserving or prevent unsafe rollback.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding the VtxoKind discriminator across the domain and database layers for on-chain Arkade UTXOs.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bob/onchain-arkade-vtxo-kind

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.
@bitcoin-coder-bob
bitcoin-coder-bob force-pushed the bob/onchain-arkade-vtxo-kind branch from be8c7fc to fc48791 Compare July 30, 2026 14:41
@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 3+ days without review. @bitcoin-coder-bob is anyone looking at this?

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 14 days without review. @bitcoin-coder-bob is anyone looking at this?

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 early Kind != VtxoKindOnchain guard is the right place.
  • IsExpired(): A zero ExpiresAt on 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 0DEFAULT 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 arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review — domain,db: add VtxoKind discriminator (part of #1159)

⚠️ PROTOCOL-CRITICAL — changes vtxo state interpretation. Flagging for human sign-off even though the logic looks correct.

What changed:
Adds (uint8 enum) to with (0, default) and . Fixes two methods:

  1. **** — 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.

  2. **** — 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 arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  1. IsNote(): on-chain Arkade UTXOs have no commitment txids, so without the kind check they'd read as notes. The guard is correct.
  2. IsExpired(): on-chain Arkade UTXOs carry a zero ExpiresAt (no batch expiry). Without the guard every such VTXO would be permanently "expired" and rejected by any check that calls IsExpired(). 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 get VtxoKindOffchain (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_vw and intent_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). ✅

@arkana-ai-bot

Copy link
Copy Markdown

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.)

@bitcoin-coder-bob
bitcoin-coder-bob marked this pull request as ready for review September 1, 2026 12:55

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 in forfeitInputs; on-chain vtxos will silently map to an empty spentBy.
  • internal/core/domain/intent.go:117 / round.go:174 — gates round state transitions on RequiresForfeit().

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 0 backfill: existing rows correctly become VtxoKindOffchain with no data migration.
  • vtxo_vw is SELECT v.* and picks up vtxo_kind automatically after recreation — no column enumeration risk.
  • intent_with_inputs_vw enumerates columns and has v.vtxo_kind added explicitly in the up migration — correct.
  • Badger vtxoDTO embeds domain.Vtxo via gob; old records decode with Kind=0 (offchain) — correct.
  • IsNote() and IsExpired() fixes are well-reasoned and tested.
  • View drop order (intent first, then vtxo_vw) is safe — intent_with_inputs_vw selects from the base vtxo table, not from vtxo_vw.
  • combinedRowToVtxo Kind wiring: Postgres uses sql.NullInt32.Int32, SQLite uses sql.NullInt64.Int64 — consistent with the respective model types.
  • No cross-repo API surface change detected: VtxoKind is internal to arkd; SDKs (ts, go, rust, dotnet) have no vtxo_kind references.

Noted / out of scope (acknowledged in PR body)

  • SelectSweepableUnrolledVtxos will surface on-chain vtxos if they ever have unrolled=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 arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.


4b3a433RequiresForfeit() guard (internal/core/domain/vtxo.go:96)

Correct. Before this commit, RequiresForfeit() was:

return !v.Swept && !v.IsNote() && !v.Unrolled

For 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.Unrolled

short-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:

  • newVtxoKindPostgresMigrator calls newMigrator().Drop() to wipe prior state, then constructs a fresh instance — correct, because Drop invalidates the migrator it ran on.
  • information_schema.columns query 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 from TestService'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.

@arkana-ai-bot

Copy link
Copy Markdown

Changes were requested on this PR (open 40+ days). @bitcoin-coder-bob need any help addressing the feedback? (VtxoKind discriminator)

@bitcoin-coder-bob
bitcoin-coder-bob dismissed arkana-ai-bot’s stale review September 3, 2026 17:37

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.

@arkana-ai-bot

Copy link
Copy Markdown

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?

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 41 days without review. @bitcoin-coder-bob is the VtxoKind discriminator still moving forward?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants