Skip to content

Verify arbitrary witness scripts and script-path spends - #77

Open
aagbotemi wants to merge 3 commits into
rust-bitcoin:masterfrom
aagbotemi:feat/script-interpreter
Open

aagbotemi wants to merge 3 commits into
rust-bitcoin:masterfrom
aagbotemi:feat/script-interpreter

Conversation

@aagbotemi

@aagbotemi aagbotemi commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Scripts outside the template paths reported inconclusive. They now go to a script interpreter, which verifies hashlocks, timelocks, and single-key witness scripts. Scripts it cannot express still report inconclusive, as the spec permits.

Rebased on #75.

Changes

  • Add verify_with_interpreter to evaluate scripts the templates cannot classify.
  • Split verify_input into template dispatch and interpreter fallback.
  • Add Error::ScriptNotSatisfied for a script that was interpreted but not satisfied.
  • verify_full_p2tr returns Inconclusive for multi-item witnesses so script-path spends reach the interpreter.

Notes for reviewers

  • Templates must return inconclusive, not an error, when they merely fail to classify a script. An error short-circuits the fallback.

Closes #76.

@aagbotemi
aagbotemi force-pushed the feat/script-interpreter branch 2 times, most recently from 077e368 to 86f8ebd Compare July 27, 2026 10:10
@aagbotemi
aagbotemi force-pushed the feat/script-interpreter branch 6 times, most recently from 7e05e83 to 718f0aa Compare August 16, 2026 15:49
@aagbotemi
aagbotemi force-pushed the feat/script-interpreter branch from 718f0aa to d3d7ce3 Compare September 28, 2026 18:52

@raphjaph raphjaph left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so we need a bunch more new tests + tweak old tests.

The first group of tests is around the core interpreter path (p2wsh):

  1. interpreter_p2wsh_single_key_valid — witness script OP_CHECKSIG (miniscript pk()), witness [sig, script], sig over the BIP-143 P2WSH sighash. Assert Verification::Valid. The basic proof the fallback works at all — currently missing.
  2. interpreter_p2wsh_unsatisfied_rejected — same script with a corrupted/missing signature (well-formed spend, interpreter parses it, sig check fails). Assert Err(Error::ScriptNotSatisfied) — and critically, never Valid. Pins the "iterator exhaustion --> satisfied" invariant the whole feature rests on.
  3. interpreter_p2wsh_hashlock_valid — OP_SHA256 OP_EQUAL with a clean [preimage, script] witness. Assert Valid. (Deliberately distinct from the existing lib.rs:1661 test, whose witness has an extra empty stack item — see "existing tests to re-verify" below.)
  4. interpreter_rejects_non_sighash_all — pk() script signed with SIGHASH_NONE. Assert Err(Error::SigHashTypeUnsupported). Mirrors verify_rejects_non_sighash_all_signatures (lib.rs:1039) for the interpreter path.
  5. interpreter_rejects_high_s — pk() script with a high-S ECDSA signature (reuse the high-S construction from lib.rs:1686). Assert Err(Error::SignatureInvalid). The interpreter itself doesn't enforce low-S; this pins the policy check at verify.rs:392.
  6. interpreter_cltv_satisfied_valid — script OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG, to_sign built with LockParams whose locktime satisfies the script and lock-enabled sequence. Assert Valid. Pins the intended timelock-verification behavior.
  7. interpreter_cltv_unsatisfied_rejected — same script, locktime below the script's value. Assert Err(Error::ScriptNotSatisfied).
  8. interpreter_inexpressible_script_is_inconclusive — a script miniscript can't structure (e.g. containing an opcode outside the miniscript fragment set). Assert Verification::Inconclusive. Pins the spec-mandated escape hatch

The next group is for taproot script-path. At the moment it can't even reach that because src/verify.rs:556–559 explicitly doesn't allow this. Your PR description states you want to add this but the code doesn't actually allow it. So here are the tests:

  1. interpreter_p2tr_script_path_valid — tr(internal_key, pk(leaf_key)) tree spent via script path (control block built with TaprootSpendInfo), schnorr sig with SIGHASH_DEFAULT. Assert Valid.
  2. interpreter_p2tr_script_path_tampered_rejected
  3. interpreter_p2tr_inexpressible_tapscript_inconclusive — valid taproot spend whose leaf isn't miniscript-expressible → Inconclusive.

Finally, we should test the legacy coverage just to be safe:

1.interpreter_p2sh_single_key_valid — bare P2SH with a pk() redeem script, scriptSig [sig, redeem_script] → Valid (exercises the legacy sighash branch of the interpreter).
2. interpreter_p2sh_p2wsh_single_key_valid — nested variant → Valid.

@aagbotemi

Copy link
Copy Markdown
Contributor Author

Thank you for the review. Group 1 and the bare-P2SH case are done. Three things before I continue.

  1. miniscript rejects the signature during iteration with InvalidEcdsaSignature, so require_low_s in the interpreter branch never fires for this input. I mapped its signature errors to SignatureInvalid instead, which also changes interpreter_p2wsh_unsatisfied_rejected. Timelocks have their own variants and could be Inconclusive. Left as-is per test 7, happy to change it.

  2. verify_standard_script dispatches P2SH on witness length, but a nested P2WSH with a single satisfaction element also has 2 items, so it routes to verify_full_p2wpkh and never reaches the interpreter. Would you want us to dispatch on the script_sig program instead.

  3. verify_full_p2tr now returns Inconclusive for multi-item witnesses so script-path spends reach the interpreter. Key-path still accepts one item only. Renamed verify_p2tr_rejects_extra_witness_items and moved the taproot timelock vector to not_valid. If that approach looks right, I'll write the group 2 tests on top of it.

@aagbotemi
aagbotemi requested a review from raphjaph September 29, 2026 11:40

This branch has not been deployed

No deployments
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.

Arbitrary scripts report inconclusive

2 participants