fix(arkd-wallet): don't silently sign a required leaf with the wrong signer key - #1121
fix(arkd-wallet): don't silently sign a required leaf with the wrong signer key#1121bitcoin-coder-bob wants to merge 3 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. Walkthrough
ChangesWallet signing behavior
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: ⚪ Minimal · up to The change prevents silent wrong-key signing and avoids a signer-only panic while preserving best-effort fallback behavior; no actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
ghost
left a comment
There was a problem hiding this comment.
Review: fix(arkd-wallet): select signer key per leaf across all multisig closures
Verdict: APPROVE on code quality — REQUESTING HUMAN REVIEW (protocol-critical: transaction signing)
This PR touches which private key signs each tapscript leaf. Wrong key = invalid signature = stuck funds or failed sweeps. Mandatory human sign-off per protocol-critical rules.
What the PR fixes (confirmed correct)
1. Missing CSV closure types in type switch — The old signerKeyForLeaf only handled MultisigClosure, CLTVMultisigClosure, and ConditionMultisigClosure. It missed CSVMultisigClosure and ConditionCSVMultisigClosure. Since sweep leaves use CSV multisig closures, any sweep of an output locked to a deprecated signer key silently used the current key and produced an invalid signature. Verified all 5 closure types in pkg/ark-lib/script/closure.go are now covered, and all embed MultisigClosure (so c.PubKeys is promoted correctly):
MultisigClosure→ directPubKeys(closure.go:26)CSVMultisigClosure→ embedsMultisigClosure(closure.go:290)CLTVMultisigClosure→ embedsMultisigClosure(closure.go:401)ConditionMultisigClosure→ embedsMultisigClosure(closure.go:500)ConditionCSVMultisigClosure→ embedsCSVMultisigClosure(closure.go:598)
2. Silent wrong-key fallback → hard error for required inputs — The required parameter maps to len(inputIndexes) > 0 in SignTransaction (service.go:~672). When the caller explicitly selects inputs (sweeps, boarding), a leaf that references none of the wallet's keys now returns an error instead of silently signing with the wrong key and failing cryptically at finalize. Best-effort callers (nil inputIndexes, i.e. bulk forfeit signing) still fall back to the current key, which keeps redundant signatures harmless. Logic is correct.
3. nil keyMgr panic fix — Old code: signingKey := w.keyMgr.forfeitPrvkey unconditionally, then overrode in signer mode. In signer-only mode (keyMgr == nil), this panicked. New code gates w.keyMgr.forfeitPrvkey behind the else branch (service.go:~679). Fix is correct.
Code quality assessment
multisigClosureKeys (service.go:~821–838) — Clean extraction. Type switch returns early per case. No allocations. Returns (nil, false) for non-multisig closures, which correctly triggers the fallback-to-current-key path in signerKeyForLeaf.
keyInLeaf (service.go:~841–851) — Nil guard on key is good defensive coding. Uses schnorr.SerializePubKey for comparison, consistent with the rest of the codebase.
signerKeyForLeaf (service.go:~797–819) — Now checks current key first, then deprecated keys. Old code only checked deprecated keys. New ordering is correct: avoids unnecessary iteration when the current key matches.
Test coverage (signer_keys_test.go:136–203) — Covers 5 scenarios:
- ✅ Current key in multisig leaf
- ✅ Deprecated key in CSV sweep leaf (the regression case)
- ✅ Required leaf with no held key → error
- ✅ Best-effort leaf with no held key → fallback
- ✅ Non-multisig leaf → fallback
Nits (non-blocking)
-
No test for
ConditionCSVMultisigClosure— The test coversCSVMultisigClosurebut notConditionCSVMultisigClosure. Since both embedMultisigClosureand the type switch handles them identically, this is low-risk, but a one-liner test case forConditionCSVMultisigClosurewould complete coverage of the exact regression surface. -
No test for
CLTVMultisigClosureorConditionMultisigClosure— Same reasoning. These were already handled in the old code, so they're not regressions, but if we're adding a comprehensiveTestSignerKeyForLeaf, covering all 5 types would be complete.
🔒 Protocol-critical: requires human approval
This PR modifies the signing key selection path for tapscript leaf signatures. The code looks correct and well-tested, but per protocol rules: a human must review and approve before merge. Bugs in key selection can produce invalid signatures that lock funds or prevent sweeps.
cc @ArkLabsHQ/protocol-reviewers
848633f to
d0913ae
Compare
ghost
left a comment
There was a problem hiding this comment.
Re-review: fix(arkd-wallet): select signer key per leaf across all multisig closures
Previous review: Requested changes (protocol-critical flag + nits on test coverage).
This review: The nits from my previous review are addressed — TestSignerKeyForLeaf now covers all 5 closure types (multisig, csv, cltv, condition, condition_csv) via the "deprecated key matched across all closure types" subtest.
Verified
- All 5 multisig closure types handled in
multisigClosureKeys(service.go:~827–845), matching all types defined inclosure.go(lines 26, 294, 405, 504, 602). No missing types. signerKeyForLeaflogic (service.go:~800–822): checks current key first, then deprecated keys, then errors onrequiredor falls back. Correct.keyInLeafnil guard (service.go:~849): defensive check on nil key prevents panic in edge cases. Good.SignTransactionwiring (service.go:~672–680):required = len(inputIndexes) > 0correctly maps sweep/boarding (explicit indexes) to hard error mode, while bulk forfeit (nil indexes) stays best-effort. Theelsebranch forkeyMgr.forfeitPrvkeyfixes the nil-deref panic in signer mode.- Test coverage: 7 subtests across
TestSignTransaction(3 cases) andTestSignerKeyForLeaf(5 cases including all closure types, error path, fallback paths).
🔒 Protocol-critical: still requires human approval
This modifies the signing key selection path for tapscript leaves. Code is correct and well-tested. A human must review and approve before merge — wrong key selection = invalid signatures = stuck funds.
LGTM from code review perspective. Approving to unblock human reviewer.
ghost
left a comment
There was a problem hiding this comment.
Re-review: fix(arkd-wallet): select signer key per leaf across all multisig closures
Previous review: Requested changes (protocol-critical flag + nits on test coverage).
This review: The nits from my previous review are addressed — TestSignerKeyForLeaf now covers all 5 closure types (multisig, csv, cltv, condition, condition_csv) via the "deprecated key matched across all closure types" subtest.
Verified
- All 5 multisig closure types handled in
multisigClosureKeys(service.go:~827–845), matching all types defined inclosure.go(lines 26, 294, 405, 504, 602). No missing types. signerKeyForLeaflogic (service.go:~800–822): checks current key first, then deprecated keys, then errors onrequiredor falls back. Correct.keyInLeafnil guard (service.go:~849): defensive check on nil key prevents panic in edge cases. Good.SignTransactionwiring (service.go:~672–680):required = len(inputIndexes) > 0correctly maps sweep/boarding (explicit indexes) to hard error mode, while bulk forfeit (nil indexes) stays best-effort. Theelsebranch forkeyMgr.forfeitPrvkeyfixes the nil-deref panic in signer mode.- Test coverage: 7 subtests across
TestSignTransaction(3 cases) andTestSignerKeyForLeaf(5 cases including all closure types, error path, fallback paths).
🔒 Protocol-critical: still requires human approval
This modifies the signing key selection path for tapscript leaves. Code is correct and well-tested. A human must review and approve before merge — wrong key selection = invalid signatures = stuck funds.
LGTM from code review perspective. Approving to unblock human reviewer.
ghost
left a comment
There was a problem hiding this comment.
Code Review: fix(arkd-wallet): select signer key per leaf across all multisig closures
Reviewer: Arkana (automated, thorough review)
Verdict: Code is correct and well-structured. Requesting human sign-off — this is protocol-critical signing code.
Summary
This PR fixes three real bugs in signerKeyForLeaf:
- Missing CSV closure types —
CSVMultisigClosureandConditionCSVMultisigClosurewere absent from the type switch, causing sweep signing with deprecated keys to silently use the wrong (current) key. The operator couldn't sweep expired batches locked to pre-rotation keys. - Silent wrong-key fallback — every "no match" returned the current key without error; failures only surfaced at finalize/broadcast as cryptic
missing signature for pubkey. - nil
keyMgrpanic —w.keyMgr.forfeitPrvkeywas dereferenced unconditionally before the signer-mode branch, panicking in signer-only mode.
All three are legitimate bugs with real consequences (stuck funds, misleading errors, crashes).
Detailed Analysis
✅ service.go:672-681 — keyMgr nil-deref fix
Before: signingKey := w.keyMgr.forfeitPrvkey evaluated unconditionally, then overwritten in signer mode. Panics when keyMgr == nil.
After: var signingKey *btcec.PrivateKey with conditional assignment in if/else branches. The keyMgr access now only happens in the else (LP mode), which is guarded by the keyMgr == nil check at service.go:461. Correct.
✅ service.go:801-828 — signerKeyForLeaf rewrite
- Returns
(*btcec.PrivateKey, error)instead of bare*btcec.PrivateKey. Allows propagating key-mismatch errors. requiredparameter:truewhen caller passes explicit input indexes (sweeps, boarding),falsefor best-effort signing. Semantics are correct —SignTransactionpassesnilindexes fromsigner_handler.go:42,SignTransactionTapscriptpasses explicit indexes fromsigner_handler.go:57.- Key priority: current key checked first, then deprecated keys. Correct — avoids signing with a deprecated key when the current one matches.
- Non-multisig leaf → fallback to current key regardless of
required. Correct — we can't check key membership for non-multisig closures.
✅ service.go:830-849 — multisigClosureKeys helper
Covers all 5 closure types registered in DecodeClosure (closure.go:36-45):
MultisigClosure✓CSVMultisigClosure✓ (embedsMultisigClosure,.PubKeyscorrect via promotion)CLTVMultisigClosure✓ConditionMultisigClosure✓ConditionCSVMultisigClosure✓ (embedsCSVMultisigClosure→MultisigClosure,.PubKeyscorrect)
No closure types missed. If a new multisig closure type is added later, it would fall through to default → false, causing a silent fallback to the current key — same as before. Acceptable, but worth a comment or a compile-time exhaustiveness check if Go ever supports it.
✅ service.go:852-861 — keyInLeaf helper
Nil-key guard at top. Uses schnorr.SerializePubKey for comparison (x-only, 32 bytes) — consistent with the old code. No issues.
✅ signer_keys_test.go:136-216 — Test coverage
Tests cover:
- Current key match ✓
- Deprecated key across all 5 closure types (including CSV and ConditionCSV — the regression surface) ✓
- Required leaf with no held key → error ✓
- Best-effort leaf with no held key → fallback ✓
- Non-multisig leaf → fallback ✓
Missing but minor: no test for keyInLeaf with nil key, though that path is simple and unlikely to regress.
Cross-repo impact
- None. Changes are internal to
pkg/arkd-wallet— no public API, type, or proto changes.signerKeyForLeaf,multisigClosureKeys, andkeyInLeafare all unexported. No downstream SDK impact.
Potential concerns (minor, non-blocking)
-
required=falsefallback is still silent. For best-effort signing, a wrong-key signature is harmless (redundant), but alog.Warnwould help debugging when an operator wonders why a forfeit signature was attempted with the wrong key. Not a blocker. -
Future closure types. If a new multisig-bearing closure type is added to
DecodeClosurebut not tomultisigClosureKeys, the wrong-key bug silently re-emerges for that type. Consider adding a comment atmultisigClosureKeysnoting it must stay in sync withDecodeClosure, or having the two share a registry. Not a blocker.
⚠️ Protocol-Critical Flag
This PR modifies transaction signing logic — specifically which private key signs tapscript leaves during sweeps and forfeits. Incorrect key selection means invalid signatures and stuck funds. The code is correct, but per policy this requires explicit human sign-off before merge.
LGTM from automated review. Awaiting human approval.
ghost
left a comment
There was a problem hiding this comment.
Code Review: fix(arkd-wallet): select signer key per leaf across all multisig closures
Reviewer: Arkana (automated, thorough review)
Verdict: Code is correct and well-structured. Requesting human sign-off — this is protocol-critical signing code.
Summary
This PR fixes three real bugs in signerKeyForLeaf:
- Missing CSV closure types —
CSVMultisigClosureandConditionCSVMultisigClosurewere absent from the type switch, causing sweep signing with deprecated keys to silently use the wrong (current) key. The operator couldn't sweep expired batches locked to pre-rotation keys. - Silent wrong-key fallback — every "no match" returned the current key without error; failures only surfaced at finalize/broadcast as cryptic
missing signature for pubkey. - nil
keyMgrpanic —w.keyMgr.forfeitPrvkeywas dereferenced unconditionally before the signer-mode branch, panicking in signer-only mode.
All three are legitimate bugs with real consequences (stuck funds, misleading errors, crashes).
Detailed Analysis
✅ service.go:672-681 — keyMgr nil-deref fix
Before: signingKey := w.keyMgr.forfeitPrvkey evaluated unconditionally, then overwritten in signer mode. Panics when keyMgr == nil.
After: var signingKey *btcec.PrivateKey with conditional assignment in if/else branches. The keyMgr access now only happens in the else (LP mode), which is guarded by the keyMgr == nil check at service.go:461. Correct.
✅ service.go:801-828 — signerKeyForLeaf rewrite
- Returns
(*btcec.PrivateKey, error)instead of bare*btcec.PrivateKey. Allows propagating key-mismatch errors. requiredparameter:truewhen caller passes explicit input indexes (sweeps, boarding),falsefor best-effort signing. Semantics are correct —SignTransactionpassesnilindexes fromsigner_handler.go:42,SignTransactionTapscriptpasses explicit indexes fromsigner_handler.go:57.- Key priority: current key checked first, then deprecated keys. Correct — avoids signing with a deprecated key when the current one matches.
- Non-multisig leaf → fallback to current key regardless of
required. Correct — we can't check key membership for non-multisig closures.
✅ service.go:830-849 — multisigClosureKeys helper
Covers all 5 closure types registered in DecodeClosure (closure.go:36-45):
MultisigClosure✓CSVMultisigClosure✓ (embedsMultisigClosure,.PubKeyscorrect via promotion)CLTVMultisigClosure✓ConditionMultisigClosure✓ConditionCSVMultisigClosure✓ (embedsCSVMultisigClosure→MultisigClosure,.PubKeyscorrect)
No closure types missed.
✅ service.go:852-861 — keyInLeaf helper
Nil-key guard at top. Uses schnorr.SerializePubKey for comparison (x-only, 32 bytes) — consistent with the old code. No issues.
✅ signer_keys_test.go:136-216 — Test coverage
Tests cover all 5 closure types, required vs best-effort semantics, and non-multisig fallback. Thorough.
Cross-repo impact
None. All changed functions are unexported. No public API, type, or proto changes. No downstream SDK impact.
Minor suggestions (non-blocking)
-
Logging on best-effort fallback. A
log.Warnwhenrequired=falsefalls back to the current key (no match found) would help operators debug unexpected forfeit failures. -
Sync guard for future closure types. If a new multisig closure type is added to
DecodeClosurebut notmultisigClosureKeys, the wrong-key bug silently re-emerges. Consider a comment noting the two must stay in sync.
⚠️ Protocol-Critical Flag
This PR modifies transaction signing logic — specifically which private key signs tapscript leaves during sweeps and forfeits. Incorrect key selection = invalid signatures = stuck funds. The code is correct, but per policy this requires explicit human sign-off before merge.
LGTM from automated review. Awaiting human approval.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required. Touches signer-side tapscript signing (key selection for VTXO co-signature); a bug here forfeits funds or DoSes rounds. Please have a protocol/wallet owner sign off before merge even though the code below reads correctly.
Correctness
The fix does what the description says and matches the call-site semantics.
pkg/arkd-wallet/core/application/wallet/service.go:672-683— the oldsigningKey := w.keyMgr.forfeitPrvkeyunconditionally dereferencedkeyMgrbefore the signer branch could rebind it, panicking in signer-only mode. The rewrite moves thekeyMgr.forfeitPrvkeyread into the LP branch, matching the mode gate at 618-623. Correct.pkg/arkd-wallet/core/application/wallet/service.go:803-826—signerKeyForLeafnow (a) decodes the leaf, (b) prefers the current key, (c) walksDeprecatedSignerKeysfor a match, (d) errors onrequiredno-match, (e) falls back to current for best-effort. This closes the silent-wrong-key path for the boarding-input case at 3361 (internal/core/application/service.go), which is the only signer-mode caller that today passes explicit indexes.pkg/arkd-wallet/core/application/wallet/service.go:830-845— the type-switch on*script.MultisigClosure,*script.CLTVMultisigClosure,*script.ConditionMultisigClosureis exact-type, so*script.CSVMultisigClosureand*script.ConditionCSVMultisigClosurereturn(nil, false)→ fall through tow.SignerKey. The PR justification (sweep is signed in LP mode viapkg/arkd-wallet/interface/grpc/handlers/wallet_handler.go:161-174,ConditionCSVMultisigClosureis never constructed anywhere — I confirmed the only occurrences are inpkg/ark-lib/script/{closure.go,verify.go,vtxo_script.go}and none construct it) holds today. If a future refactor ever routes a CSV leaf into signer mode this fallback becomes a silent-wrong-key regression again — worth a comment on the switch pinning the assumption.
required heuristic
required := len(inputIndexes) > 0 matches today's callers correctly:
- Required (explicit indexes):
internal/core/application/service.go:3361(boarding inputs of commitment tx),internal/infrastructure/tx-builder/covenantless/sweep.go:185— but sweep goes through the LP-mode wallet handler, so the signer-moderequired=truebranch is only exercised by boarding-input signing today. - Best-effort (nil indexes):
internal/core/application/service.go:1030(ark tx),:1072(checkpoint tx),internal/core/application/fraud.go:167(broadcast forfeit). These all funnel a signed PSBT intobuilder.FinalizeAndExtract, which will surface a missing-sig failure if the wrong key was picked — so the exact "cryptic error later" ux this PR is fixing for boarding still exists on these paths. Out of the stated scope but worth a follow-up:fraud.go:167in particular signs a single-VTXO forfeit tx where "we know what leaf we're co-signing" and could plausibly opt intorequired=true.
Tests
pkg/arkd-wallet/core/application/wallet/signer_keys_test.go:141-210 covers current-key match, deprecated-key match across all three handled closure types, required-no-match error, best-effort fallback, and non-multisig fallback. Reasonable coverage of the changed function. Missing but low-value: an explicit case for a CSVMultisigClosure leaf falling through (to pin the switch assumption), and a keyInLeaf nil-key case.
Nits
service.go:820-823error message is user-facing on a signing failure — good specificity (tells the operator a rotated key wasn't retained). Fine as-is.signer_keys_test.go:180line-length is on the edge; not a blocker.
|
⏰ This PR has been open for 46 days without a review. @bitcoin-coder-bob is anyone looking at this? (It's a security-relevant fix — silently signing with the wrong signer key.) |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review: fix(arkd-wallet): don't silently sign a required leaf with the wrong signer key
Bug
signerKeyForLeaf previously returned the current SignerKey as a fallback when no deprecated key matched the leaf's multisig closure. For a required signing input (explicit index list), this produced a silent wrong-key signature — a signature that would fail script validation on-chain but was indistinguishable from a correct one at the application layer.
Fix
- Splits into three helpers:
multisigClosureKeys(decodes the leaf type),keyInLeaf(checks membership),signerKeyForLeaf(orchestrates with arequired boolparameter). - When
required=trueand no key matches, returns an error immediately rather than signing with the wrong key. - When
required=false(best-effort path, no explicit index list), still falls back to the current key — preserving the existing behaviour for non-required inputs. - The call site passes
len(inputIndexes) > 0asrequired, which correctly distinguishes required from best-effort signing.
Security assessment
This is a correctness fix, not a key exposure issue. The wrong signature would fail at script validation, so funds are not at risk of theft. However, silent wrong-key signing could cause rounds to fail mysteriously (commitment tx rejected) and masks key-rotation bugs. The error message is clear and actionable.
Code quality
multisigClosureKeysis a clean refactor — the type switch is now readable and the non-multisig path returns(nil, false)explicitly.keyInLeafcorrectly usesschnorr.SerializePubKeycomparison (byte-level, not pointer equality).nilkey guard inkeyInLeafprevents a panic if a deprecated key entry has a nil key.
Looks ready to merge after human sign-off. No blocking issues.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana automated review — 2026-08-06
What the bug was: signerKeyForLeaf checked deprecated keys correctly but only checked whether the deprecated key appeared in the leaf — it didn't check the current key first. More importantly, the function always returned w.SignerKey as the default, meaning if neither current nor deprecated keys matched the leaf (e.g. after a key rotation where the deprecated key list was not retained), a wrong-key signature was silently produced, resulting in an invalid forfeit/tapscript signature that would be reported as an error only at verification time.
Correctness of the fix:
multisigClosureKeyscleanly extracts pubkeys from all three multisig closure types (MultisigClosure,CLTVMultisigClosure,ConditionMultisigClosure) via a type switch.keyInLeafdoes the comparison using schnorr serialization (x-only, 32 bytes) — correct for tapscript.signerKeyForLeafchecks current key first, then each deprecated key in order.- The
requiredflag is key: when signing explicitinputIndexes(the caller knows exactly what they're signing), a key mismatch is a hard error. For best-effort signing (no explicit indexes), fallback to current key preserves existing behaviour. This is the right safety/compatibility tradeoff.
Tests:
- Covers current key match, deprecated key match for all three closure types (the regression surface), required+no-match→error, best-effort+no-match→fallback, non-multisig→fallback. Comprehensive.
Ready to merge from a correctness standpoint. Human sign-off required given this is tapscript signing.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — signerKeyForLeaf: don't silently sign required leaf with wrong key
Correct fix; the logic restructuring is clean.
Before: In SignModeSigner, the code set signingKey = w.keyMgr.forfeitPrvkey up front, then optionally reassigned from signerKeyForLeaf. Inside signerKeyForLeaf, iteration over DeprecatedSignerKeys would match but the current SignerKey was never checked — so a leaf referencing only the current key would fall through to it by accident (returning w.SignerKey at the end). A leaf referencing none of the wallet's keys would silently use the current key and produce an invalid signature.
After: Clear priority: current key → deprecated keys → error (required) or fallback (best-effort). The decomposition into multisigClosureKeys and keyInLeaf helpers removes the duplication and makes the logic auditable.
Test coverage: TestSignerKeyForLeaf covers current-key match, all three multisig closure types for deprecated key selection (the regression surface), no-match required error, no-match best-effort fallback, and non-multisig fallback. Complete.
Looks ready to merge.
…ures Introspect CSV and condition-CSV closures too so deprecated-key sweeps use the right key, and error when a required leaf references none of the wallet's keys instead of silently signing with the current one. Also stop dereferencing keyMgr in signer mode.
The signer only co-signs collaborative multisig leaves (plain, CLTV, condition). Sweep/exit CSV closures key off the wallet's stable forfeit key and are signed in liquidity-provider mode, never via signerKeyForLeaf; ConditionCSVMultisigClosure is never constructed anywhere. Drop the CSVMultisigClosure and ConditionCSVMultisigClosure cases and their test coverage.
d00197d to
73e21d4
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Follow-up pass (d00197d → 73e21d4): 2 prior issues closed (1 fixed, 1 no longer applicable), 0 remain open. Net change is test-only.
Prior-issue close-out
Nit 1 — No test for CLTVMultisigClosure or ConditionMultisigClosure
→ Fixed in 73e21d4. TestSignerKeyForLeaf now has a table-driven sub-test ("deprecated key matched across all closure types") that exercises MultisigClosure, CLTVMultisigClosure, and ConditionMultisigClosure against a deprecated signer key. Exactly the regression surface called out.
Nit 2 — No test for ConditionCSVMultisigClosure
→ No longer applicable. The "drop dead CSV closure cases" commit (d00197d on the old branch, now part of 73e21d4 after rebase) correctly removes CSV handling from signerKeyForLeaf entirely. The justification in the commit message is sound: sweep/exit CSV closures are signed in liquidity-provider mode via keyMgr.forfeitPrvkey, never via signerKeyForLeaf. Testing them here would be testing dead code on the wrong path. The nil-guard on the LP path at service.go:618 (if signMode == application.SignModeLiquidityProvider && w.keyMgr == nil { return … }) means keyMgr.forfeitPrvkey at line 682 is safe.
Incremental diff assessment
The only code change since d00197d is in signer_keys_test.go: addition of TestSignerKeyForLeaf and its arklib import. service.go is unchanged from last pass.
signer_keys_test.go — TestSignerKeyForLeaf (signer_keys_test.go:136–210)
The function is clean. Specific observations:
txscriptwas already imported forTestSignTransaction— usingtxscript.OP_TRUEfor the condition byte is safe (signer_keys_test.go:17).- Map iteration over the
closuresmap is non-deterministic, but eacht.Runsub-case is fully independent and the result does not depend on ordering — no issue. - The
pubhelper closure compares Schnorr-serialised keys consistently with the production code path (keyInLeafalso usesschnorr.SerializePubKey). Correct. - Minor gap (non-blocking): There is no test asserting that
signerKeyForLeaf(leaf, false)where the leaf contains a deprecated key still returns the deprecated key rather than falling back to current. The production code is correct (key matching happens before therequiredgate, sorequired=falsedoes not skip the deprecated-key scan), but this branch is currently only exercised implicitly. A one-line sub-case would complete the matrix.
No new findings
No new protocol, security, or correctness findings in the incremental diff. The core fix (correct key selection for required vs. best-effort leaves; nil-keyMgr guard; three-type switch) was assessed in the prior pass and is unchanged.
Human protocol review still required before merge — see prior comment for rationale.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review · commit 73e21d4
Fixes a signing correctness bug in the arkd-wallet: previously, when a tapscript leaf referenced a deprecated signer key, signerKeyForLeaf would fall through to the current SignerKey and silently produce a signature that the verifier would reject. This was invisible at signing time and would only surface as a broadcast failure.
What the fix does
- Refactors
signerKeyForLeafto first check the current key, then each deprecated key, against the leaf's multisig closure pubkeys. - If no held key is found and the caller passed explicit input indexes (
required=true), returns a hard error rather than a silent wrong-key signature. - Best-effort callers (
required=false) still fall back to the current key (unchanged behaviour for non-critical paths). - Covers all three multisig closure types:
MultisigClosure,CLTVMultisigClosure,ConditionMultisigClosure.
Test coverage
TestSignerKeyForLeaf covers: current key match, deprecated key match across all three closure types (the actual regression surface), required leaf with no held key errors, best-effort leaf with no held key falls back, non-multisig leaf falls back.
One thing to verify
The required flag is determined by len(inputIndexes) > 0 at the call site. Confirm this is the right discriminator — in particular, that a caller that passes an empty inputIndexes slice but still needs a matching key will not silently receive the wrong key.
Looks correct. Ready for human review.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — protocol-critical flag
Fix: correct key selection when signing required tapscript leaves
The bug: the old signerKeyForLeaf searched deprecated keys but not the current key against the leaf's multisig closure. If the leaf referenced the current key, it was implicitly handled only because the fallback at the end returned w.SignerKey — but so did the 'no match found' case. This meant a leaf referencing no held key at all silently produced a signature with the wrong key instead of an error.
The fix correctly checks the current key first, then deprecated keys, then errors (required) or falls back (best-effort). The logic is now explicit about what each path means.
What looks good:
multisigClosureKeyscleanly separates key extraction from selection logickeyInLeafnil-guards the private key before serializing, preventing a panicrequired boolparameter surfaces the semantics the caller already knew: explicit input indexes → required, nil → best-effort- Tests cover all three closure types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure) and all branch outcomes — this is exactly the regression surface
One note: the 'non-multisig leaf falls back to current key (required=true)' test passes without error. The comment says non-multisig leaves can't be matched, so we default to the current key. This is correct for forfeit inputs (which use a different code path) but could silently produce a wrong signature if a future caller passes a non-multisig leaf with required=true. The current callers are safe; worth a comment if this function becomes more widely used.
🚨 Protocol-critical path — touches signing on every tapscript leaf. Needs a human sign-off before merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — #1121
fix(arkd-wallet): don't silently sign a required leaf with the wrong signer
What changed
signerKeyForLeaf now:
- Decodes the leaf's multisig closure and checks if the current SignerKey is in it first (the old code skipped this and went straight to deprecated keys).
- Then checks deprecated keys.
- If no wallet key matches and
required=true(caller supplied explicit input indexes), returns an error instead of falling back to the current key. required=false(best-effort pass) still falls back to the current key.
Correctness
The silent wrong-key path was a real problem: signing a leaf with a key that isn't in its multisig closure produces a valid-looking PSBT update but an unspendable transaction. The new error path makes the failure explicit so the caller or operator can act.
The required parameter is wired to len(inputIndexes) > 0 at the call site — precise: when the caller explicitly nominated an input for signing, a key mismatch is fatal; otherwise it's best-effort.
Checking the current key before deprecated keys is also correct; the current key is the common case and should be fast-pathed.
Tests
TestSignerKeyForLeaf in signer_keys_test.go covers: current key in leaf, deprecated key in leaf, no key matches (required → error, not-required → current key fallback), and non-multisig leaves. Good coverage.
Notes
- The error message is clear about the likely cause (key rotation without retaining the old key as deprecated).
keyInLeafcorrectly comparesschnorr.SerializePubKey(x-only), which is what the tapscript multisig closure uses.
Verdict: Correct fix, well-tested. Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
This PR has been open for 6+ days without review. @bitcoin-coder-bob is anyone looking at this?
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — fix(arkd-wallet): don't silently sign with wrong signer key
What the fix does:
The old had two problems:
- When the wallet had no deprecated keys () it returned the current without checking whether it's actually referenced by the leaf. This was usually correct, but implicit.
- When deprecated keys were present and none matched, it silently fell back to the current signer key even if that key wasn't in the leaf either. A rotated key that wasn't retained as a deprecated key would cause the wallet to produce a signature that doesn't match any key in the leaf multisig — the tx would be invalid and the error would only surface at broadcast or verification time, not at signing.
The new code:
- Explicitly checks the current against the leaf's multisig public keys first
- Then checks each deprecated key in order
- If neither matches and the caller passed explicit input indexes (), it returns a descriptive error immediately rather than producing a silently wrong signature
The flag is at the call site, which correctly distinguishes "I know exactly which inputs need signing" (hard error) from "sign what you can" (best-effort fallback).
Test coverage: covers current key, deprecated key across all three closure types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure), required-no-match error, best-effort fallback, and non-multisig leaf. Complete.
Nit: The last commit message says "drop dead CSV closure cases" but the diff I see retains the CLTV/Condition closure handling. Presumably the "dead" cases were something else (perhaps in a different file not shown in this summary diff) — worth a quick look to confirm the scope.
Overall: looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review — fix(arkd-wallet): signer key leaf guard
Verdict: looks ready to merge. Security-relevant fix.
The previous signerKeyForLeaf only matched deprecated keys against leaf public keys but never checked the current SignerKey first — it always fell through to return w.SignerKey at the bottom. The fix checks current key before deprecated, so a leaf that references the current key is signed correctly without also iterating deprecated keys.
More importantly, a required=true leaf that references none of the wallet's keys now returns a hard error instead of silently producing a signature under the wrong key. A wrong-key signature on a protocol transaction (VTXO tree, forfeit, checkpoint) would be invalid on-chain; catching it early is strictly better.
What changed:
signerKeyForLeafrefactored into three helpers:signerKeyForLeaf,multisigClosureKeys,keyInLeaf.required boolparameter: callers that pass explicit input indexes get an error on mismatch; best-effort callers fall back to current key.- Non-multisig leaves (unrecognised closure type, parse error) fall back silently — correct, since those don't need a specific key.
Tests cover all three closure types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure), the required-error path, the best-effort fallback, and the non-multisig fallback. Comprehensive. ✅
One minor nit: the error message says "a rotated signer key may not have been retained as a deprecated key". This is the most likely cause, but it's also possible someone passed the wrong PSBT altogether. Consider wording like "the leaf references a public key that is not the current or any deprecated signer key" to keep the message purely diagnostic without implying a specific root cause. Not blocking.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha 73e21d4
This is a correctness/security fix in the tapscript leaf key selection path.
What changed: signerKeyForLeaf now checks whether the leaf's multisig closure actually references one of the wallet's keys (current or deprecated). Before this change, if no deprecated key matched, the function silently fell through to the current key regardless of what the leaf expected — a wrong-key signature that would never satisfy the script.
The fix is correct:
- Checks current key first, then deprecated keys.
- If the leaf references none of the wallet's keys:
required=truereturns an error (explicit index list, operator must act);required=falsefalls back to current key (best-effort path, caller handles failure). - Non-multisig leaves fall back to current key (unchanged behaviour, correct since those use a different signing mechanism).
Test coverage is thorough: covers current key match, deprecated key match across all three closure types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure), required-missing → error, best-effort-missing → fallback, non-multisig → fallback.
One minor nit: the function name remains signerKeyForLeaf but it now returns (*btcec.PrivateKey, error). The callers that don't check the error should be audited to confirm there are none after this PR (the single call site in SignTransaction does check it).
Looks ready to merge. No protocol-critical concerns — this is in the wallet package, not the tree-signing path.
…-guard One conflict, in signer_keys_test.go, and it is an add/add: this branch adds TestSignerKeyForLeaf for the new signerKeyForLeaf(leaf, required) signature, master adds TestSignTransactionRejectsPartialSigHashTypes. Neither touches the other, so both are kept. Both sides ran into a shared trailing "})\n}", so ours needed its own subtest and function closers before master's top-level func. Dropping the markers alone left the file unparseable.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
PROTOCOL-CRITICAL: human review required.
Follow-up pass (73e21d4 → d0dcb66): 0 prior issues newly fixed in this increment; 1 minor non-blocking gap remains open. New findings: 0 blocking, 1 documentation nit.
Prior-issue close-out
Nit 1 — No test for CLTVMultisigClosure or ConditionMultisigClosure
→ Fixed (tracked as Fixed in the previous pass at 73e21d4; TestSignerKeyForLeaf in this diff confirms the table-driven sub-tests exercise both types under "deprecated key matched across all closure types").
Nit 2 — No test for ConditionCSVMultisigClosure
→ No longer applicable (tracked in previous pass; CSV types were dropped from signerKeyForLeaf scope as dead code; that decision stands and is correct).
Minor gap — No required=false test where leaf contains a deprecated key
→ Still open. The "deprecated key matched across all closure types" sub-tests all use required=true. The production code is correct (key matching happens before the required guard, so required=false does not skip the deprecated-key scan), but this branch is untested. One additional sub-case would close it. Non-blocking.
Incremental diff assessment (73e21d4 → d0dcb66)
service.go:677–683 — call site
signerKeyForLeaf now takes required bool = len(inputIndexes) > 0 and the error is checked and propagated. ✅
A nil or empty inputIndexes both map to required=false via len. An empty non-nil slice means !slices.Contains([], i) is always true so the loop body is never reached — signerKeyForLeaf is never called in that case anyway. ✅
service.go:802–868 — helpers
multisigClosureKeys: clean type switch, returns (nil, false) for decode errors and non-multisig closures. Three handled types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure) are the correct scope for signer mode; CSV types are signed in LP mode via keyMgr.forfeitPrvkey. ✅
keyInLeaf: nil guard on key prevents panic on a nil deprecated-key entry. schnorr.SerializePubKey comparison (32-byte x-only) is correct for tapscript. ✅
signerKeyForLeaf: priority is current key → deprecated keys → error (required) or fallback (best-effort). Correct. ✅
Documentation nit — required semantics for non-multisig leaves (service.go:803–814)
When multisigClosureKeys returns (nil, false) (non-multisig or decode failure), signerKeyForLeaf returns (w.SignerKey, nil) unconditionally — even when required=true. The required flag is silently ignored for non-multisig leaves. The existing test "non-multisig leaf falls back to current" passes required=true and correctly documents this behavior, but the function's doc comment ("Non-multisig leaves also fall back to the current key") does not note that required is ignored in this path. A future caller passing required=true with a non-multisig leaf would receive the current key with no error and no indication that key matching was skipped. Consider adding a note to the doc comment. Non-blocking.
signer_keys_test.go:136–213 — TestSignerKeyForLeaf
Five sub-cases (current key match, deprecated key across 3 closure types, required-no-match error, best-effort fallback, non-multisig fallback). Map iteration over closures is non-deterministic but each sub-test is independent — no ordering concern. pub helper uses schnorr.SerializePubKey consistent with production comparison. txscript.OP_TRUE is a valid single-byte condition for the ConditionMultisigClosure sub-case. ✅
No new blocking findings
No new protocol, security, or correctness issues in the incremental diff. The core fix — explicit key selection with hard error on mismatch for required inputs — is correct, well-scoped, and well-tested.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
First review — arkd-wallet#1121 (don't silently sign a required leaf with the wrong signer key)
This is a correctness/security fix in the wallet signing path. Summary:
What changed: signerKeyForLeaf now searches both the current and all deprecated keys for a match against the leaf's multisig closure pubkeys before falling back. When required=true (caller passed explicit input indexes) and no held key matches, it returns an error rather than silently returning the current key and producing an invalid or wrong-keyed signature.
Assessment:
- The old fallthrough to
SignerKeywas wrong after key rotation: a leaf referencing a deprecated key would get signed with the current key, producing a signature that fails verification at the multisig closure. - The fix correctly separates the match search from the decode/fallback logic via the new
multisigClosureKeys/keyInLeafhelpers — cleaner than the previous loop. - The
requiredparameter is a good API: best-effort callers (no explicit indexes) still get the old fallback, so the change is backward-compatible for that case. - The test suite is thorough: covers current key, deprecated key across all three closure types (multisig, CLTV multisig, condition multisig), required-with-no-match error, best-effort fallback, and non-multisig leaf.
- One observation: the error message says "a rotated signer key may not have been retained as a deprecated key" — this is good operational guidance but operators should ensure their runbooks cover the key rotation → deprecated-key retention flow.
Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — fix(arkd-wallet): don't silently sign with the wrong signer key (sha d0dcb66)
Verdict: looks ready — security fix, clear logic, well tested. Flag for human sign-off before merge.
What the bug was
The old signerKeyForLeaf searched deprecated keys but never checked whether the current SignerKey actually matches the leaf's multisig closure — it always returned the current key as the fallback. When performing key rotation, a leaf referencing the deprecated key but whose current-key scan reached the fallback branch would produce a signature under the wrong key, silently fail CHECKSIG, and leave the input unsigned.
What the fix does
- Extracts
multisigClosureKeysso key lookup is shared across all closure types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure) — removes the previous silent fallback for unrecognised types. - Checks the current key first, then walks deprecated keys — so an operator that hasn't rotated pays no overhead.
- Distinguishes
requiredvs best-effort inputs: a required input that matches no held key returns an error instead of a wrong-key signature; a best-effort one still falls back to the current key.
Test coverage
TestSignerKeyForLeaf covers:
- All three multisig closure types for the deprecated-key path (the regression surface)
- Required + no-held-key → error
- Best-effort + no-held-key → fallback to current
- Non-multisig leaf → fallback to current
That's thorough for the changed surface area.
Minor notes
keyInLeafguardskey == nil, which is defensive but correct.- The
requiredflag is inferred fromlen(inputIndexes) > 0at the call site — this is a bit implicit; a comment explaining why that proxy is correct would help a future reader, but it isn't wrong.
|
This PR has been open for 4-5+ days without a review. @bitcoin-coder-bob is anyone looking at this? (fix(arkd-wallet): don't silently sign a required leaf with the wrong signer key; 4 days without review.) |
|
This PR has been open for 10+ weeks without a review. @bitcoin-coder-bob — is this still blocked on something upstream? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — 2026-09-02
Protocol-critical — signing key selection. Human reviewer required before merge.
signerKeyForLeaf refactor
The old logic only checked DeprecatedSignerKeys (never the current SignerKey) and fell back to the current key silently when none matched, which produced a wrong-key signature with no error. The fix checks the current key first, then deprecated keys, and returns an error when a required leaf matches none of the wallet keys.
The split into multisigClosureKeys + keyInLeaf + signerKeyForLeaf is clean.
required vs best-effort distinction
Passing required=true when inputIndexes is non-empty (explicit signing request) vs false for a best-effort pass is correct. The best-effort path still falls back to the current key to preserve the old behaviour for unknown leaf types.
TestSignerKeyForLeaf
Covers: current key in leaf, deprecated key across all three closure types (multisig, CLTV, condition), required-but-no-match error, best-effort fallback, and non-multisig leaf. The "deprecated key matched across all closure types" case directly pins the regression surface.
One gap: the test suite does not cover the path where both current and a deprecated key appear in the same leaf (e.g. a 2-of-2 where the signer key was rotated and both old and new are in the script). Does signerKeyForLeaf return the current key or the deprecated one in that case? The code returns the first match (current), which is probably fine, but worth a comment.
Overall: looks correct and ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana automated review — #1121
Bug fixed: signerKeyForLeaf previously checked only DeprecatedSignerKeys when the deprecated list was non-empty, and if none matched it silently returned w.SignerKey — the current key. If the leaf actually referenced a rotated key that was not retained as a deprecated key, the wallet would sign the leaf with the wrong private key, producing an invalid signature. For a required input (explicit index list), this is now a hard error instead of a silent bad signature.
Changes:
signerKeyForLeaf(leafScript, required bool)now checksw.SignerKeyfirst, then deprecated keys, then returns an error ifrequiredand no match. The old logic only fell through toSignerKeyat the bottom.multisigClosureKeysextracted as a pure function — clean.keyInLeafextracted — also clean, nil-safe.requiredislen(inputIndexes) > 0at the call site; best-effort calls (no explicit indexes) still fall back to current key.
Test coverage: TestSignerKeyForLeaf covers current key match, deprecated key match, no-match-required (error), no-match-best-effort (fallback), and non-multisig leaf (fallback). Comprehensive.
One observation: The error message says "a rotated signer key may not have been retained as a deprecated key." For a production deployment this is an operator-recoverable error, not a client-visible one. Confirm the error surface (does it propagate to the gRPC caller or is it logged internally?).
Looks ready to merge. The fix is strictly safer than the old silent behaviour.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Review: looks ready to merge — fix silent wrong-key signing in arkd-wallet (🛡 security-relevant, flagged for human review)
Bug fixed: signerKeyForLeaf previously fell through to w.SignerKey when the deprecated-key loop found no match, even on a leaf that explicitly names a different key. This meant a required signing input would produce a syntactically valid but cryptographically wrong signature — the wallet silently signed with the current key instead of a deprecated one, which would fail at validation. After a key rotation where the old key was not retained as a deprecated entry, forfeits signed against old VTXOs would be silently corrupt.
Fix is correct:
multisigClosureKeysextracts the leaf's public keys without side-effects.keyInLeafgives a clean predicate over both current and deprecated keys.- The
requiredflag distinguishes explicit signing requests (hard error) from best-effort scans (fall back to current key), which is the right split. - All three multisig closure types (plain, CLTV, condition) are handled — the test explicitly covers all three to prevent future regressions.
Tests: comprehensive — covers current key match, deprecated match across all closure types, required-but-no-match error, best-effort fallback, and non-multisig leaf fallback. All test cases are focused and readable.
One gap to consider: the error message says "a rotated signer key may not have been retained as a deprecated key" — it might be worth emitting a structured log or metric at that point so ops can catch key-rotation procedure mistakes in production before forfeits fail.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha d0dcb66
Fixes a silent wrong-key signature in SignTransaction.
Before: signerKeyForLeaf fell through to the current SignerKey whenever no deprecated key matched. For a required input (explicit input indexes), this produced an invalid signature silently — the transaction would fail on broadcast, with no error at signing time.
After: signerKeyForLeaf takes a required bool. When required=true and no wallet key (current or deprecated) is found in the leaf's multisig closure, it returns a hard error. When required=false (best-effort path), it falls back to the current key as before.
Helper decomposition is clean: multisigClosureKeys decodes the closure and returns keys; keyInLeaf does the comparison. The nil guard on key is a good defensive touch.
Tests in TestSignerKeyForLeaf cover: current key found, deprecated key across all three closure types (MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure — previously all fell through to current key as the regression), required with no held key → error, best-effort with no held key → fallback, non-multisig leaf → fallback.
Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana automated review — sha d0dcb66cc
Good fix. The root problem: when a required tapscript leaf references a key the wallet no longer holds (e.g., after key rotation where the old key wasn't listed as deprecated), the old code silently fell through to signing with the current key. That produces an invalid signature and potentially a broken transaction.
The change: now takes a boolean. When (explicit input indexes were passed by the caller), and the leaf's multisig closure references none of the wallet's keys, the function returns an error rather than a wrong-key signature. When (best-effort path), it still falls back to the current key to preserve existing behaviour.
The refactoring into + helpers is clean and makes the key-matching logic testable in isolation.
Tests (): covers all closure types (multisig, CLTV, condition), the current-key path, deprecated-key paths, the required-but-no-key error, the best-effort fallback, and non-multisig leaves. That's thorough regression coverage for the regression surface.
One note: the error message points operators toward the key-rotation issue ('a rotated signer key may not have been retained as a deprecated key'), which is the right place to look.
Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana automated review — sha d0dcb66cc
Good fix. The root problem: when a required tapscript leaf references a key the wallet no longer holds (e.g., after key rotation where the old key was not retained as deprecated), the old code silently fell through to signing with the current key, producing an invalid signature.
The change: signerKeyForLeaf now takes a required boolean. When true (explicit input indexes passed by the caller) and the leaf's multisig closure references none of the wallet's keys, the function returns an error rather than a wrong-key signature. When false (best-effort path), it still falls back to the current key to preserve existing behaviour.
The refactoring into multisigClosureKeys + keyInLeaf helpers is clean and makes the key-matching logic testable in isolation.
Tests (TestSignerKeyForLeaf): covers all closure types (multisig, CLTV, condition), the current-key path, deprecated-key paths across all three closure types, the required-but-no-key error, the best-effort fallback, and non-multisig leaves. Thorough regression coverage.
The error message pointing operators toward the key-rotation issue ('a rotated signer key may not have been retained as a deprecated key') is the right level of detail for diagnosing on-call.
Looks ready to merge.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — sha d0dcb66
What this fixes:
The old signerKeyForLeaf only checked deprecated keys against the leaf and fell through to the current SignerKey if none matched — meaning the current key could be used to sign a leaf it does not appear in. This PR fixes that by:
- Checking the current key against the leaf first, then deprecated keys.
- When the leaf is "required" (caller passed explicit inputIndexes), returning an error if no held key matches rather than silently producing a wrong-key signature.
- When the leaf is "best-effort", falling back to the current key (preserving prior behaviour for code paths that do not know which leaf they will sign).
Correctness:
- multisigClosureKeys correctly handles all three closure types the server co-signs: MultisigClosure, CLTVMultisigClosure, ConditionMultisigClosure. Non-multisig leaves (e.g. plain hash locks) fall back to the current key — correct, those are not co-signed.
- keyInLeaf uses schnorr.SerializePubKey for comparison — correct for Taproot leaf pubkeys.
- The required/best-effort split is threaded correctly: len(inputIndexes) > 0 implies required.
Tests:
TestSignerKeyForLeaf covers: current key in leaf, deprecated key across all three closure types, required leaf with no held key (must error), best-effort with no held key (must fall back), non-multisig leaf (must fall back). That is exactly the regression surface.
One question: When required=true and no key matches, the error message says "a rotated signer key may not have been retained as a deprecated key". Should this also trigger an alert / metric so an operator knows a signing attempt failed for this reason? A silent error return at the RPC layer might be hard to diagnose in production.
Ready to merge after human review. This is a security-correctness fix for signing.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — #1121 (sha d0dcb66)
What was wrong: signerKeyForLeaf checked deprecated keys by comparing against the leaf's closure keys, but it checked the current key first only if there were no deprecated keys at all. When deprecated keys existed, the search looped over them but never checked the current key explicitly — so a leaf referencing the current key would fall through to returning w.SignerKey anyway only if no deprecated key matched, meaning the selection logic was by accident rather than by design. More importantly, a leaf referencing none of the wallet's keys silently produced a wrong-key signature.
What the fix does:
- Checks the current key first, then deprecated keys — in a clearly readable order.
- When a leaf is required (caller passed explicit input indexes) and no wallet key matches, returns a hard error instead of a silent wrong-key signature.
- Extracts
multisigClosureKeysandkeyInLeafas testable helpers. - Non-multisig leaves and best-effort inputs still fall back to the current key (unchanged behaviour for those cases).
Test coverage: TestSignerKeyForLeaf covers: current key in leaf, deprecated key across all three multisig closure types (the regression surface), required leaf with no matching key → error, best-effort fallback, non-multisig fallback. Good.
One observation: The error message in the required branch mentions "rotated signer key may not have been retained as a deprecated key" which is a useful operator hint. The caller receives this error as a SignTransaction failure, so the RPC returns an error rather than silently producing an invalid signature. ✓
Ready to merge.
What
signerKeyForLeafpicks which of the wallet's signer keys (the current one or a retained deprecated one) signs a given tapscript leaf in signer mode. This hardens it.Silent wrong-key fallback. Every "no match" path returned the current
SignerKeywithout checking whether that key is actually in the leaf. A leaf requiring a key the wallet no longer holds was signed with the current key anyway; signing succeeded with no error and the failure only surfaced later, at finalize/broadcast, as a crypticmissing signature for pubkey .... Arequiredparameter (set when the caller passes explicit input indexes, e.g. boarding-input signing) now makes a no-match leaf a hard error instead. Best-effort callers (nil indexes) keep falling back, so redundant signatures stay harmless.nil
keyMgrpanic. The call site dereferencedw.keyMgr.forfeitPrvkeyunconditionally before the signer-mode branch overwrote it, panicking in signer-only mode (this is what crashed the existingTestSignTransaction). The dereference now happens only in liquidity-provider mode.How
signerKeyForLeafto extract the leaf's pubkeys (multisigClosureKeys) and return the current or a deprecated key, whichever the leaf references, for the multisig closures the signer co-signs:MultisigClosure,CLTVMultisigClosure,ConditionMultisigClosure.requiredparameter and return an error when no held key matches a required leaf.keyMgrin signer mode.Scope / out of scope
CSVMultisigClosureleaves are not signed through this path: the sweep closure keys off the wallet's stable, seed-derived forfeit key (builder.gobuilds it fromGetForfeitPubkey) and is signed in liquidity-provider mode, never viasignerKeyForLeaf. Signing a sweep with a key the primary wallet doesn't hold is handled separately by the primary/fallback wallet iteration in Sweep fallback across primary and fallback arkd-wallets #1101.ConditionCSVMultisigClosureis never constructed anywhere in the codebase. An earlier revision of this PR addedCSVMultisigClosureandConditionCSVMultisigClosurecases on a sweep rationale that does not hold (sweeps never reach this function); they have been dropped as dead code, which leaves the handled closure set the same asmaster.master, and only touchespkg/arkd-wallet.Tests
TestSignerKeyForLeaf: current-key match, deprecated-key match across the handled multisig closure types (multisig / CLTV / condition), required-no-match error, best-effort fallback, non-multisig fallback.TestSignTransaction(previously panicking on nilkeyMgr) now passes.TestDeprecatedSignerKeyandTestReactToFraudis unchanged by dropping the CSV cases.Summary by CodeRabbit
Bug Fixes
Tests