Skip to content

Terminal methods on StatefulSmartContract produce invalid BIP-143 sighash subscript #42

@E-Jacko

Description

@E-Jacko

Summary

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.

Why this isn't surfaced by existing tests

integration/rust/tests/auction.rs::test_auction_close:

  1. Uses deadline = 0 (see nLockTime hardcoded to 0 in call-tx builder blocks deadline-based contracts #40nLockTime is hardcoded to 0, so close requires deadline=0 to even attempt).
  2. 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.

Affected files

  • packages/runar-rs/src/sdk/contract.rs (subscript-trim conditional + get_code_sep_index helper)

The TS SDK has the same gap in packages/runar-sdk/src/contract.ts.

Root cause

Two interlocking conditions:

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

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

PR with fix is opened alongside this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions