Skip to content

chore: update regtest to latest, emulator v0.0.7 support - #192

Merged
d4rp4t merged 6 commits into
arkade-os:masterfrom
d4rp4t:feat/regtest-update
Aug 28, 2026
Merged

d4rp4t merged 6 commits into
arkade-os:masterfrom
d4rp4t:feat/regtest-update

Conversation

@d4rp4t

@d4rp4t d4rp4t commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46212514-bf6d-4486-ba29-cc95ef4cf441


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.

@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

Two changes: removal of ARK_CONTAINER=ark from .env.regtest, and a regtest submodule bump (15354f99 → 56dca6ed).

ARK_CONTAINER=ark removal — safe

The removed line was inert. Tracing both consumers:

  • regtest submodule (regtest.mjs:307): hardcodes docker exec arkd … — ARK_CONTAINER is never read.
  • dotnet tests (DockerHelper.cs:477): Container.Arkd = "arkd" — also hardcoded, never reads the env var.

The compose stack (docker/compose.ark.yml) fixes container_name: arkd, so the override to "ark" would have been actively wrong if anything had read it. The accompanying comment ("SDK tests hardcode 'ark' for docker exec") was stale — the tests hardcode "arkd", not "ark". Removal is correct.

Submodule bump — looks fine

The bump pulls in update .env.defaults (#57), which advances ARKD_IMAGE and ARKD_WALLET_IMAGE to v0.9.16 (from v0.9.15 in the outgoing commit). No structural changes to compose topology, container names, or ports that would affect test harness assumptions. No cross-repo API surface is touched.

No protocol-critical paths involved

This change is entirely infrastructure/config. VTXO handling, signing, forfeit, round lifecycle, and unilateral exit paths are unaffected. No concerns.

Emulator v0.0.7 (arkade-os/emulator#129, "validate checkpoints and
prevouts") walks every input of a submitted Arkade transaction or intent
proof and hard-fails on a missing prevarktx field; v0.0.6 only validated
the fields a PSBT happened to carry, so zero fields meant zero checks.
The SDK never attached the field, so once the regtest submodule bumped
the emulator to v0.0.7 every POST /v1/tx failed with "missing prevout tx
for input 0".

The transaction an Arkade input must carry is the checkpoint's parent,
not the checkpoint: input i spends checkpoint i, which spends the coin,
so it is the VTXO's own creating transaction. The emulator reconciles it
against the checkpoint's witness utxo.

Only the previous transaction's outputs are read (txid, value, script,
introspection opcodes), all witness-independent, so a PSBT's unsigned
global transaction resolves correctly and no signature propagation has
to be waited on. The field also lives in the PSBT unknown map, which no
sighash covers, so it is attached after signing, at submit time.

- IPrevArkTxProvider / PrevArkTxProvider resolve previous transactions
  from IVirtualTxStorage, then arkd's indexer, then chain. Indexer
  results are keyed by the txid parsed out of each body, since arkd
  returns them in database order rather than request order, and bodies
  that were not requested are dropped.
- ArkadePsbtExtensions.AttachPrevArkTxsAsync / AttachIntentPrevArkTxsAsync
  annotate an Arkade transaction and a BIP322 intent proof respectively;
  the proof's synthetic input 0 is left alone. An input already carrying
  the field keeps it, since the emulator rejects an input bearing two.
- ArkadeEmulatorSpendSubmitter annotates before submitting;
  AddArkadeEmulator registers the provider.
- IBitcoinBlockchain.GetRawTransactionAsync, defaulted to
  NotSupportedException and implemented for Esplora and NBXplorer.
  Boarding and commitment parents have no off-chain body, so an intent
  proof registering a boarding input cannot be annotated without it.

Unresolvable parents throw with the input index and txid named rather
than letting the emulator reject the submission.

The batch tree-signing hop still submits to POST /v1/tx with an empty
checkpoint list, which v0.0.7 rejects before it looks at prevouts; that
needs a different shape and is documented as a limit.
@d4rp4t d4rp4t changed the title chore: update regtest to latest chore: update regtest to latest, emulator v0.0.7 support Aug 27, 2026
8c20170 commented out the e2e-swaps job but left it in publish's needs
list, so Actions rejected the whole workflow file — "Job 'publish'
depends on unknown job 'e2e-swaps'" — and CI failed at startup in 0s
without running a single job.

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

Status: 2 commits since last pass (infra-only: .env.regtest + submodule bump). Prior review raised 0 protocol findings — nothing to close out. This pass covers the substantive new work: emulator v0.0.7 prevarktx support.


Prior findings

None open. Previous review covered .env.regtest removal and submodule bump only. All clean.


New findings

1. RpcBlockchain does not override GetRawTransactionAsync — doc claim is wrong [Bug]

NArk.Core/Blockchain/RpcBlockchain.cs implements IBitcoinBlockchain but has no GetRawTransactionAsync override (confirmed: 193-line file, public methods end at EstimateFeeRateAsync). The new interface default (IBitcoinBlockchain.cs:120-122) throws NotSupportedException.

The interface XML doc states: "every in-box backend overrides it" — that is incorrect; RpcBlockchain is an in-box backend and does not.

What happens at runtime: PrevArkTxProvider.ResolveAsync catches Exception when e is not OperationCanceledException on the per-txid chain call (IPrevArkTxProvider.cs chain loop). NotSupportedException matches that guard, gets logged at Debug, and is silently treated as a miss. AttachResolvedAsync then throws InvalidOperationException naming the input index and txid — which is correct behaviour (fail before submit, not at the emulator) but the debug log gives no indication the real cause is a missing override, making this hard to diagnose in the field.

RpcBlockchain already holds an RPCClient; the fix is a direct lift from NBXplorerBlockchain.GetRawTransactionAsync (same getrawtransaction / verbose=false call pattern). Fix the implementation or correct the doc.


2. ArkadeBatchSessionExtension.CoSignAsync is broken against emulator v0.0.7, with no code guard [Protocol]

ArkadeBatchSessionExtension.cs routes PostTreeSigning through CoSignWithEmulatorAsync, which:

  • passes an empty checkpoint list to POST /v1/tx
  • does not call AttachPrevArkTxsAsync, so no prevarktx fields are set

Emulator v0.0.7 requires exactly one checkpoint per input and the prevarktx field on every input before it inspects anything else. Both are absent. The emulator will reject with missing prevout tx for input N (or a checkpoint count error) on the first submission.

The docs acknowledge this (docs/articles/arkade-script.md, last bullet): "The batch tree-signing hop does not carry checkpoints… That hop therefore needs the checkpoint-carrying shape (or its own endpoint) before it can work against a current emulator." But there is no code gate: a caller who wires AddArkadeEmulator and runs a batch against a v0.0.7 emulator will reach the emulator with a malformed submission. The failure surfaces at the emulator, not at registration time.

Recommend adding a throw new NotSupportedException(...) in CoSignAsync at PostTreeSigning (analogous to the existing guard at PreForfeitFinalization) until the endpoint shape is resolved. The forfeit guard's rationale — "failing loudly beats silently producing an invalid result" — applies equally here.


3. CoSignWithEmulatorAsync is silently incompatible with emulator v0.0.7 [Minor]

ArkadePsbtExtensions.CoSignWithEmulatorAsync is public API. It does not call AttachPrevArkTxsAsync. Any caller who uses this helper directly against a v0.0.7 emulator will be rejected with missing prevout tx for input N. The README now documents the manual call pattern (await arkTx.AttachPrevArkTxsAsync(...) before CoSignWithEmulatorAsync) but the method's XML doc has no mention of this.

Add a <para> note: emulator v0.0.7+ requires prevarktx on every input; call AttachPrevArkTxsAsync / AttachIntentPrevArkTxsAsync before this method if the submission target is v0.0.7+.


4. e2e-swaps CI job silently disabled [Tracking]

.github/workflows/build.yml: e2e-swaps job is commented out as "deprecated". If swap paths covered any forfeit exit or round-lifecycle scenarios, those are now uncovered in CI. Please ensure the intent is tracked — deprecating a CI job without a replacement leaves a coverage gap that won't be noticed until a regression ships.


What looks good

Protocol logic in AttachPrevArkTxsAsync: The indirection is correct — prevarktx on Arkade input i is the transaction funding the checkpoint's single input (the VTXO/boarding tx), not the checkpoint itself. The emulator holds the checkpoint and uses prevarktx to validate the checkpoint's spend. The code correctly resolves this via checkpoint.Inputs[0].PrevOut.Hash.

AttachIntentPrevArkTxsAsync: Correctly skips input 0 (BIP322 message input, emulator synthesises its prevout) and annotates inputs 1..N directly from their PrevOut.Hash.

Response ordering defence in PrevArkTxProvider: arkd's GetVirtualTxs returns hexes in DB order, not request order. The code keys each result by the txid parsed from the body, not by position in the response array — correct and tested (Provider_KeysIndexerResultsByParsedTxid_NotRequestOrder).

Supply-chain defence: wanted.Contains(txid) ensures a server responding with an unrequested transaction cannot attach it to an input. Tested (Provider_DropsTransactionsThatWereNotRequested).

Txid verification in GetRawTransactionAsync (Esplora + NBXplorer): Both implementations parse the response and verify tx.GetHash() == txid rather than trusting the label on the response. Correct.

Test coverage (PrevArkTxTests.cs, 375 lines): All critical paths covered — happy path, signature preservation, ordering defence, storage priority, chain fallback, indexer failure with chain fallback, indexer failure without chain fallback, idempotent field, serialisation format (no segwit marker in prevarktx value), unparseable hex tolerance. The Danger "source changed with no test changes" warning appears to have been evaluated before PrevArkTxTests.cs was counted.

IBitcoinBlockchain.GetRawTransactionAsync default: Throwing NotSupportedException with the type name in the message is the right default — custom implementors get a clear error at the point of failure. The TryAddSingleton guard in EmulatorServiceCollectionExtensions correctly prevents double-registration.

PSBT mutation + NSubstitute assertion in SpendSubmitter_SubmitAsync_ForwardsArkTxAndCheckpointsToEmulator: arkTx.ToBase64() is evaluated after AttachPrevArkTxsAsync mutates it in place, so the assertion correctly matches what was submitted (annotated PSBT). The added Assert.That(arkTx.Inputs[0].GetArkFieldPrevArkTx(...)) separately confirms the mutation occurred. Both correct.

RpcBlockchain did not override GetRawTransactionAsync, contradicting the
interface doc's claim that every in-box backend does. It holds an
RPCClient already, so it gets the same getrawtransaction/verbose=false
implementation as the NBXplorer backend.

A backend that genuinely cannot serve raw transactions was swallowed
per-txid at debug level, so the caller's "could not be resolved" error
named the symptom and hid the cause. PrevArkTxProvider now reports a
NotSupportedException once, at warning level, naming the backend and the
missing override, and stops retrying the remaining txids.

ArkadeBatchSessionExtension routed PostTreeSigning to POST /v1/tx with an
empty checkpoint list and no prevarktx fields. Emulator v0.0.7 requires
one checkpoint per input plus the field on every input before it inspects
anything else, and a tree transaction has neither, so the hop reached the
emulator with a request it cannot accept. It now throws
NotSupportedException at the call site, matching the existing
PreForfeitFinalization guard.

CoSignWithEmulatorAsync attaches no prevarktx and submits no checkpoints,
so it is subject to the same rejection. Its XML doc now says so and
points at AttachPrevArkTxsAsync / AttachIntentPrevArkTxsAsync.

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

Status: 2 commits since last pass (fixing 3 gaps in emulator v0.0.7 support). All 4 prior findings closed. 1 dead-code observation and 2 minor concerns are new.


Prior findings

1. RpcBlockchain missing GetRawTransactionAsync override — Fixed
The override is now present (RpcBlockchain.cs:172–204) using the same getrawtransaction/verbose=false pattern as GetTxStatusAsync, with txid verification. The IBitcoinBlockchain doc is corrected: "the in-box Esplora, NBXplorer and Bitcoin Core RPC backends all override it."

2. ArkadeBatchSessionExtension.CoSignAsync broken for PostTreeSigning — Fixed
PostTreeSigning now throws NotSupportedException at ArkadeBatchSessionExtension.cs:102–113 before the emulator is contacted. The guard is placed after PreForfeitFinalization (correct order) and includes the checkpoint-shape rationale. The class-level XML doc is updated to reflect both phases throwing. Test replaced by CoSign_RefusesTreeTransactions_RatherThanSubmittingAMalformedRequest, which asserts the exception and confirms SubmitTxAsync is never called.

3. CoSignWithEmulatorAsync XML doc silent on prevarktx — Fixed
The added <para> warns callers that emulator v0.0.7+ requires prevarktx on every input and points at AttachPrevArkTxsAsync/AttachIntentPrevArkTxsAsync. It also flags the empty checkpoint list limitation.

4. e2e-swaps CI job silently disabled — No longer applicable
Removing e2e-swaps from publish.needs is the correct follow-through to the deprecation already in the previous commit. The underlying coverage gap was noted in the last review; nothing new here.


New findings

1. Emulator signing loop in CoSignAsync is unreachable dead code [Minor]

ArkadeBatchSessionExtension.cs:116–130 — the signed = new PSBT[psbts.Count]; for (var i …) loop is unreachable. BatchExtensionPhase has exactly two values (PostTreeSigning = 1, PreForfeitFinalization = 2), both of which throw before reaching this code. The path through the loop would require an arkade-bound input set and a phase that is neither of those two, which cannot happen with the current enum.

The code isn't harmful, but it is confusing: a maintainer adding a new phase later might not realise the loop is already there, or might rely on it without understanding it was never exercised. Options: remove the loop and leave a comment saying "add the submission loop here when a submittable phase is introduced", or add a default: throw new ArgumentOutOfRangeException(nameof(phase)) above it to make the unreachability explicit.

2. Network.Main hardcoded in RpcBlockchain.GetRawTransactionAsync [Minor]

RpcBlockchain.cs:195: Transaction.Parse(hex, Network.Main). The RPCClient already carries _client.Network, so the network the backend is connected to is available. The comment acknowledges this is "network-independent" for serialisation purposes and the txid verification (tx.GetHash() == txid) prevents the function from returning a wrong result. In practice this is safe. Consistency and explicitness favour _client.Network — one word change.

3. Provider_BackendWithoutRawTxSupport_IsAMissNotACrash only covers single-txid case [Minor]

PrevArkTxTests.cs — the test uses a single txid, so whether break fires or not doesn't affect the outcome. The valuable property of the new break (one warning for N txids, no per-txid retry storm when the backend structurally cannot serve raw transactions) is not covered. This is low priority but worth a follow-up test case with two wanted txids confirming only one warning is emitted and both are absent from resolved.


What looks good

PrevArkTxProvider.NotSupportedException handler (IPrevArkTxProvider.cs:151–163): catches before the general Exception guard, logs at Warning (not Debug) naming the backend type and the remediation (override GetRawTransactionAsync), then breaks to avoid per-txid repetition. Exactly right.

RpcBlockchain implementation details: verbose=false returns raw hex (not a decoded JSON object), response.Error is not null checked before parsing, txid verified against the returned transaction hash — matches the Esplora/NBXplorer pattern and the security rationale from the last review.

Test CoSign_RefusesTreeTransactions_*: Asserts message contains "checkpoint" (correct signal) and explicitly confirms SubmitTxAsync was never invoked. Removing CoSign_PropagatesEmulatorFailure is correct — that test exercised the emulator loop which is now unreachable.

docs/articles/arkade-script.md: Updated correctly: the two-bullet "not implemented" notes are merged into a single accurate description covering both phases and both reasons.

…rdcoded networks

CoSignAsync's per-PSBT emulator loop was unreachable: BatchExtensionPhase
has two values and both throw above it. A maintainer adding a third phase
would have found a loop that had never run. The two guards become a
switch over the phase whose default arm throws ArgumentOutOfRangeException,
so an unhandled phase fails loudly and the place a future submission arm
belongs is named rather than implied.

RpcBlockchain and NBXplorerBlockchain both know the network they are
connected to, so they parse against it instead of Network.Main. Esplora is
constructed from a bare base URI and has none; its comment now says that
rather than reading as an oversight.

The backend-without-raw-tx-support test used a single txid, so it could
not tell whether the loop gives up after the first structural failure.
A second case uses two txids and asserts one chain call and one warning.

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

Status: 1 commit since last pass (c6cf291 — refactor: make unreachable co-sign path explicit, drop hardcoded networks). All 3 prior findings fixed. 0 new findings.


Prior findings

1. Dead-code unreachable signing loop — Fixed (c6cf291)
The loop is removed entirely. The two sequential if (phase ==) guards are consolidated into a single throw phase switch { ..., _ => new ArgumentOutOfRangeException(...) } expression. The _ arm carries an explicit message guiding whoever adds the next phase, which is exactly the remediation suggested. Clean.

2. Network.Main hardcoded in RpcBlockchain and NBXplorerBlockchain — Fixed (c6cf291)
RpcBlockchain.cs:193 now uses _client.Network; NBXplorerBlockchain.cs:288 uses _explorerClient.Network.NBitcoinNetwork. Both are the right references for their respective client types. EsploraBlockchain retains Network.Main with an updated comment explaining the constraint (constructed from a bare URI — no network object available); that is correct.

3. Provider_BackendWithoutRawTxSupport_IsAMissNotACrash only covered the single-txid case — Fixed (c6cf291)
New test Provider_BackendWithoutRawTxSupport_GivesUpAfterTheFirstTxid (PrevArkTxTests.cs:294–327) supplies two txids, asserts chain.Received(1).GetRawTransactionAsync(...) (exactly one RPC call — the break fires), and asserts logger.Received(1).Log(LogLevel.Warning, ..., Arg.Any<NotSupportedException>(), ...) (exactly one warning, carrying the exception). Both assertions are correct against the NSubstitute ILogger pattern.


What looks good in this commit

Switch consolidation in CoSignAsync: Collapsing the two guards into throw phase switch { ..., _ => new ArgumentOutOfRangeException(...) } is strictly better — the _ arm makes exhaustiveness explicit, the message tells the next developer exactly what to do, and the dead signing loop is gone rather than just commented. No behaviour change for existing callers.

_emulator field retention comment: The inline comment explaining why the field stays even though nothing currently submits is accurate and saves the next reviewer from flagging it as dead code.

Esplora Network.Main comment: The revised comment ("Unlike the RPC and NBXplorer backends, this one is constructed from a bare base URI and so has no network to hand") correctly explains the asymmetry rather than repeating the prior vague claim about network-independence.


No open findings remain. The three refactors in this commit are mechanical, correct, and well-tested.

@d4rp4t
d4rp4t merged commit d626612 into arkade-os:master Aug 28, 2026
12 checks passed
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