You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Rust SDK's BIP-143 sighash-subscript construction for terminal methods on StatefulSmartContract subclasses computes a subscript that does not match what the on-chain script will hash at validation time.
The on-chain script trims its subscript at the method's OP_CODESEPARATOR position (per BIP-143). The SDK builds the sighash using the wrong subscript range for terminal methods (e.g. Auction.close), producing a signature that fails CHECKSIG with arc error 461 — Signature must be zero for failed CHECK(MULTI)SIG operation (NULLFAIL) on mainnet ARC broadcasters.
The state-mutating bid() method (different code path) works correctly — only the terminal close() path is affected.
Runs against regtest via RPCProvider::new_regtest(), which has looser script-validation policy than mainnet ARC broadcasters.
When the locktime fix from #40 enables a non-zero deadline, and the test is broadcast against a mainnet ARC endpoint (TAAL, GorillaPool, etc.), the close call returns NULLFAIL.
The TS SDK has the same gap in packages/runar-sdk/src/contract.ts.
Root cause
Two interlocking conditions:
Subscript trim condition over-narrowly gated on is_stateful: the BSV consensus rule (BIP-143 derived) is that any OP_CODESEPARATOR in the script before OP_CHECKSIG resets the subscript regardless of whether the contract has state fields. The SDK was only applying the trim when is_stateful was true.
get_code_sep_index falls back to a legacy adjusted offset when invoked through from_txid with the actual on-chain script (rather than a synthetic test deploy). For terminal methods, the index returned wasn't the actual byte position of the method's OP_CODESEPARATOR.
Proposed fix
Two-part fix to packages/runar-rs/src/sdk/contract.rs:
Part 1 — drop the is_stateful term from the trim condition. Trim whenever there's an OP_CODESEPARATOR in the script (code_sep_idx >= 0), regardless of whether the contract is stateful. Per BIP-143 + BSV consensus, this is the correct condition.
Part 2 — replace get_code_sep_index's legacy fallback with a robust find_codesep_offsets walker that:
Walks the actual on-chain locking script byte-by-byte
Correctly handles all BSV push opcodes (0x01..0x4b, OP_PUSHDATA1/2/4)
Returns the actual byte position of each OP_CODESEPARATOR
Evidence
A patched SDK produces signatures that validate against on-chain CHECKSIG for terminal methods. A test transaction broadcast to BSV mainnet mines successfully (779ea679…3606 at block 949222). Without the patch, identical transactions are rejected by every ARC broadcaster with NULLFAIL.
Summary
The Rust SDK's BIP-143 sighash-subscript construction for terminal methods on
StatefulSmartContractsubclasses computes a subscript that does not match what the on-chain script will hash at validation time.The on-chain script trims its subscript at the method's
OP_CODESEPARATORposition (per BIP-143). The SDK builds the sighash using the wrong subscript range for terminal methods (e.g.Auction.close), producing a signature that fails CHECKSIG witharc error 461 — Signature must be zero for failed CHECK(MULTI)SIG operation(NULLFAIL) on mainnet ARC broadcasters.The state-mutating
bid()method (different code path) works correctly — only the terminalclose()path is affected.Why this isn't surfaced by existing tests
integration/rust/tests/auction.rs::test_auction_close:deadline = 0(seenLockTimehardcoded to 0 in call-tx builder blocks deadline-based contracts #40 —nLockTimeis hardcoded to 0, so close requires deadline=0 to even attempt).RPCProvider::new_regtest(), which has looser script-validation policy than mainnet ARC broadcasters.When the locktime fix from #40 enables a non-zero deadline, and the test is broadcast against a mainnet ARC endpoint (TAAL, GorillaPool, etc.), the close call returns NULLFAIL.
Affected files
packages/runar-rs/src/sdk/contract.rs(subscript-trim conditional +get_code_sep_indexhelper)The TS SDK has the same gap in
packages/runar-sdk/src/contract.ts.Root cause
Two interlocking conditions:
Subscript trim condition over-narrowly gated on
is_stateful: the BSV consensus rule (BIP-143 derived) is that anyOP_CODESEPARATORin the script beforeOP_CHECKSIGresets the subscript regardless of whether the contract has state fields. The SDK was only applying the trim whenis_statefulwas true.get_code_sep_indexfalls back to a legacy adjusted offset when invoked throughfrom_txidwith the actual on-chain script (rather than a synthetic test deploy). For terminal methods, the index returned wasn't the actual byte position of the method'sOP_CODESEPARATOR.Proposed fix
Two-part fix to
packages/runar-rs/src/sdk/contract.rs:Part 1 — drop the
is_statefulterm from the trim condition. Trim whenever there's anOP_CODESEPARATORin the script (code_sep_idx >= 0), regardless of whether the contract is stateful. Per BIP-143 + BSV consensus, this is the correct condition.Part 2 — replace
get_code_sep_index's legacy fallback with a robustfind_codesep_offsetswalker that:OP_CODESEPARATOREvidence
A patched SDK produces signatures that validate against on-chain CHECKSIG for terminal methods. A test transaction broadcast to BSV mainnet mines successfully (
779ea679…3606at block 949222). Without the patch, identical transactions are rejected by every ARC broadcaster with NULLFAIL.PR with fix is opened alongside this issue.