Skip to content

Implement verification states, time locks and required consensus rules - #73

Merged
raphjaph merged 6 commits into
rust-bitcoin:masterfrom
aagbotemi:feat/verification-states
Aug 24, 2026
Merged

raphjaph merged 6 commits into
rust-bitcoin:masterfrom
aagbotemi:feat/verification-states

Conversation

@aagbotemi

@aagbotemi aagbotemi commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implement the spec's three-state verifier output, time lock support for full signatures, explicit enforcement of the required consensus rules, and spec-compliant prevout resolution for proof of funds.

Rebased on #71

Changes

  • Add Verification enum (Valid { time, age } / Inconclusive).
  • Add internal InputVerification for per-input outcomes.
  • Add LockParams to set nLockTime and nSequence on to_sign.
  • All verify_* functions now return Result<Verification>, with the invalid state reported as Err.

Notes for reviewers

  • Uninterpretable scripts (taproot script-path spends, non-multisig witness scripts, unknown script types) report Inconclusive rather than erroring.
  • The upgradeable version rule (nVersion must be 0 or 2) is checked after input verification, so a required-rule failure (e.g. bad signature) still reports invalid even on an unsupported version.
  • LockParams version is derived — 2 when any lock is set, else 0, so an inconsistent version can't be constructed. Threaded through sign_full, sign_full_encoded, sign_pof, sign_pof_encoded and create_to_sign. The simple variant keeps all-zero locks per the spec.
  • Resolve proof-of-funds prevouts from PSBT UTXO fields instead of a caller-supplied parameter. Proof-of-funds prevout resolution includes the spec's same-txid non-witness UTXO fallback.

Closes #72

@aagbotemi
aagbotemi force-pushed the feat/verification-states branch 2 times, most recently from b3d5c8c to a186d35 Compare July 27, 2026 09:03
@aagbotemi
aagbotemi force-pushed the feat/verification-states branch 5 times, most recently from 0f048d5 to facd4c9 Compare August 16, 2026 14:53
@aagbotemi
aagbotemi force-pushed the feat/verification-states branch from facd4c9 to 5d13173 Compare August 16, 2026 15:40

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

It's been a while since I worked on the library and the BIP has been finalized now (which included a bunch of verification changes), so I'm just paging back everything into memory. Your PRs have also been sitting for a while and some of the following things might have been lost in the stuff I merged or are still upcoming in the other PRs you have.

This PR looks correct overall, some things to change/add:

  1. nVersion upgradeable rule never reports Inconclusive
  2. sign_pof doesn't set inputs[0].witness_utxo for segwit challenges
  3. Unused error variant Error::UnknownScriptType, let's remove it, just use Ok(Inconclusive)

Tests:

  • verify_full_rejects_noncanonical_to_sign has case(|tx| tx.version = Version(3)); asserting Err(ToSignInvalid) (src/lib.rs:602), this should be Ok(Inconclusive) I think
  • add a test for sign a PoF with LockParams and assert Valid { time, age }
  • The InputVerification::Inconclusive => return Ok(Verification::Inconclusive) early-return in verify_pof's input loop is untested. Add a PoF whose proof input's script is uninterpretable (e.g. a witness-v2 program, or a non-multisig P2WSH) → Inconclusive. This also covers the parse_multisig failure → Inconclusive behavior change (previously Err), which nothing currently asserts
  • add a test for LOW_S rejection
  • add a test for MINIMALDATA rejection
  • Same-txid non_witness_utxo fallback. The or_else fallback over earlier inputs in verify_pof is entirely untested. Two cases: positive -> two legacy PoF inputs spending different vouts of the same prev transaction, non_witness_utxo only on the first -> Ok; negative -> same setup but the second input's UTXO absent (or vice versa) -> Err(Error::ToSignInvalid).

Comment thread src/error.rs Outdated
Comment on lines +101 to +102
#[snafu(display("Cannot interpret script `{script}`"))]
UnknownScriptType { script: String },

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.

This doesn't seem to be used anywhere

Comment thread src/util.rs Outdated

/// Enforces the LOW_S rule, a valid ECDSA signature must have a low-S value.
#[allow(clippy::result_large_err)]
pub fn require_low_s(signature: &bitcoin::secp256k1::ecdsa::Signature) -> Result<()> {

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.

Let's add a test for this

Comment thread src/verify.rs
Comment on lines +185 to +197
match verify_input(&to_sign, &[challenge_prevout], 0)? {
InputVerification::Inconclusive => Ok(Verification::Inconclusive),
InputVerification::Valid => {
// Upgradeable rule: nVersion must be 0 or 2, else inconclusive.
if to_sign.version != Version(0) && to_sign.version != Version(2) {
return Ok(Verification::Inconclusive);
}
Ok(Verification::Valid {
time: to_sign.lock_time,
age: to_sign.input[0].sequence,
})
}
}

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.

This is good but we have the check_to_sign call before so this code never gets executed and therefore Ok(Inconclusive) never returned. Up to you how to structure this, we could combine it into check_to_sign or change the check_to_sign. Whatever is more ergonomic

@aagbotemi

Copy link
Copy Markdown
Contributor Author

Thank you for the review @raphjaph. Fixed in 22fb304

@aagbotemi
aagbotemi requested a review from raphjaph August 23, 2026 00:32

@sdmg15 sdmg15 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tACK 22fb304

Regarding the required rules, maybe I missed it, but are we checking the CLEANSTACK and MINIMALIF?

Comment thread src/verify.rs Outdated
/// Verifies a BIP-322 full proof of funds from a spec-compliant string encoding.
/// Verifies a BIP-322 full proof of funds.
///
/// See [`verify_pof`] for the expected contents of `prevouts`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: given that prevouts was removed from this function maybe we can remove this comment too.

@raphjaph

raphjaph commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

tACK 22fb304

Regarding the required rules, maybe I missed it, but are we checking the CLEANSTACK and MINIMALIF?

MINIMALIF is N/A (we never execute scripts; non-template scripts are Inconclusive). CLEANSTACK was enforced everywhere except P2TR — now fixed; duplicate outpoints in PoF are also rejected. Stale doc line removed.

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

LGTM

@raphjaph
raphjaph merged commit f366453 into rust-bitcoin:master Aug 24, 2026
13 checks passed
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.

Implement verifier states, time locks, and required consensus rules

3 participants