fix(sdk): sign user Sig params at primary covenant input for stateful contracts#111
Open
E-Jacko wants to merge 1 commit into
Open
fix(sdk): sign user Sig params at primary covenant input for stateful contracts#111E-Jacko wants to merge 1 commit into
E-Jacko wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
build_stateful_unlockinprepare_call's second pass signsSig-typed user-params only for extra inputs (input_idx > 0), leaving the primary covenant input's signature slots filled with the 72-byte all-zero placeholder. Stateful contracts whose method body verifies a usercheck_sigat the primary input fail on-chain script evaluation and get rejected at the relay.Problem
packages/runar-rs/src/sdk/contract.rs:883-897on upstream HEAD (a8187ab4):The
if input_idx > 0guard is a correctness bug for contracts that look like:The method body has TWO signature checks at input 0:
compute_op_push_tx_with_code_sep.check_sig(sig, &self.issuer)against the signer's pubkey — this needs the SDK to sign a BIP-143 sighash for the unlock atinput_idx == 0, which the guard prevents.Pre-fix, on-chain
check_sig(<72 zero bytes>, issuer_pubkey)returnsfalse, the surroundingassert!fails, the script evaluation returns false, and the relay rejects the transaction.How this surfaced
Surfaced when building a stateful covenant contract whose method body asserts
check_sig(sig, &self.issuer)directly against a constructor-pinned issuer pubkey. The transaction built cleanly client-side, the OP_PUSH_TX preimage was structurally valid (verified via byte-level decode — hashOutputs / hashPrevouts / hashSequence / scriptCode all matched the contract-side recompute), but the unlock script for input 0 contained 72 contiguous zero bytes at the sig placeholder offset. ARC rejected with "Script failed" / stickySEEN_IN_ORPHAN_MEMPOOLstatus.The contract pattern (OP_PUSH_TX guard + user check_sig at the primary covenant input) is a natural shape for any stateful covenant where the contract author wants to bind a specific signing identity into the state transition — issuer-signed mints, owner-required state updates, role-gated method dispatch, etc.
Fix
Remove the
if input_idx > 0guard. Sign Sig user-params for ALL inputs includinginput_idx == 0. The BIP-143 sighash signed for the primary input is identical to the OP_PUSH_TX sighash for that input (same tx, same input_idx, same trimmed subscript), so both checks in the unlock context see consistent message bytes — no preimage divergence risk.Pure guard removal — no other behavioural changes. Contracts that don't declare a user Sig param see no change (the inner
for &idx in sig_indicesloop body is skipped whensig_indicesis empty).Verification
Reproducible in 60 seconds:
Mainnet evidence (pre-fix): Diagnostic byte-level decode of a rejected stateful-contract broadcast showed the unlock script for input 0 contained 72 contiguous zero bytes at the sig placeholder offset. ARC's rejection diagnostic confirmed the script eval failed at the user check_sig assertion (
Script failed/SEEN_IN_ORPHAN_MEMPOOLsticky status). With the fix applied, the same transaction shape mines successfully.On dedicated regression test: the cleanest test shape would extract a small
pub fn sign_stateful_sig_params(signer, tx, input_idx, subscript, sats, code_sep_idx, sig_indices, resolved_args)helper (same pattern as PR #105'sresolve_continuation_satoshisextraction) so the test can assert directly that calling the helper withinput_idx == 0populatesresolved_args[idx]with a non-placeholder signature. Happy to author the helper extraction + inline test in a follow-up PR if the maintainer prefers that shape — the 4-line guard removal in the current PR is mechanical and visually inspectable from the diff, but the explicit regression coverage is a reasonable ask.Backwards compatibility
Behaviour change is narrowly scoped:
Siguser-params:sig_indicesis empty; the loop body is skipped; no change for input 0 or any other input.Siguser-params and are called WITHOUT mixing OP_PUSH_TX preimage verification (rare for stateful contracts but possible for stateless): no on-chain effect — the OP_PUSH_TX path isn't exercising input 0's checkSig anyway.Siguser-params AND use them incheck_sigat the primary input: now the unlock script contains a real signature instead of zero bytes; previously these methods could not broadcast.No existing successful broadcast path is invalidated.
Related
fix(sdk): honor explicit outputSatoshis user parameter) in that both fixes correct an under-applied case inprepare_call's second pass. Same call-site, adjacent code regions.SdkValue::EmptySigfor OR-CHECKSIG branches): both surface in contracts that compose explicit signature predicates with the SDK's covenant machinery. The fixes are orthogonal — this PR ensures the matching branch ALWAYS gets a real signature at input 0; [feat] SdkValue::EmptySig variant for OR-CHECKSIG branched authorization #106 lets a non-matching OR-branch declare an empty-sig slot.