Skip to main content

mini_bitcoin_script/
script.rs

1use crate::engine::{execute_on_stack, ExecuteOpts};
2use crate::error::ScriptError;
3use crate::stack::{is_true, Stack};
4use crate::tokenizer::parse_script;
5
6/// Validates a Pay-to-Public-Key-Hash (P2PKH) script pair.
7///
8/// Executes `script_sig` (the unlocking script) on a fresh stack, then
9/// executes `script_pubkey` (the locking script) on the resulting stack.
10/// This two-phase model matches Bitcoin's actual execution behavior
11/// (post-2010), preventing scriptSig from manipulating scriptPubKey's
12/// control flow.
13///
14/// Returns `Ok(true)` if the combined execution succeeds (top stack
15/// element is truthy after both phases).
16///
17/// OP_CHECKSIG uses stub mode (always succeeds). For real ECDSA
18/// verification, use [`validate_p2pkh_with_opts`] with a sighash and
19/// the `secp256k1` feature enabled.
20///
21/// Both arguments are raw script bytes (not hex). Use
22/// [`crate::hex::decode_hex`] to convert hex strings first.
23pub fn validate_p2pkh(script_sig: &[u8], script_pubkey: &[u8]) -> Result<bool, ScriptError> {
24    validate_p2pkh_with_opts(script_sig, script_pubkey, &ExecuteOpts::default())
25}
26
27/// Validates a P2PKH script pair with execution options.
28///
29/// See [`validate_p2pkh`] for details. The `opts` parameter controls
30/// OP_CHECKSIG behavior via [`ExecuteOpts::sighash`].
31pub fn validate_p2pkh_with_opts(
32    script_sig: &[u8],
33    script_pubkey: &[u8],
34    opts: &ExecuteOpts,
35) -> Result<bool, ScriptError> {
36    let sig_tokens = parse_script(script_sig)?;
37    let pk_tokens = parse_script(script_pubkey)?;
38
39    let mut stack = Stack::new();
40
41    // Phase 1: execute scriptSig (pushes sig + pubkey onto stack)
42    execute_on_stack(&sig_tokens, &mut stack, opts)?;
43
44    // Phase 2: execute scriptPubKey on the resulting stack
45    execute_on_stack(&pk_tokens, &mut stack, opts)?;
46
47    // Final evaluation
48    if stack.is_empty() {
49        return Ok(false);
50    }
51    let top = stack.pop()?;
52    Ok(is_true(&top))
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::hash;
59
60    /// Builds a scriptSig that pushes a fake signature and a public key.
61    fn build_script_sig(sig: &[u8], pubkey: &[u8]) -> Vec<u8> {
62        let mut script = Vec::new();
63        // Push signature (direct push: length byte + data)
64        assert!(sig.len() <= 0x4b);
65        script.push(sig.len() as u8);
66        script.extend_from_slice(sig);
67        // Push public key
68        assert!(pubkey.len() <= 0x4b);
69        script.push(pubkey.len() as u8);
70        script.extend_from_slice(pubkey);
71        script
72    }
73
74    /// Builds a standard P2PKH scriptPubKey:
75    /// OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG
76    fn build_script_pubkey(pubkey_hash: &[u8; 20]) -> Vec<u8> {
77        let mut script = Vec::new();
78        script.push(0x76); // OP_DUP
79        script.push(0xa9); // OP_HASH160
80        script.push(0x14); // Push 20 bytes
81        script.extend_from_slice(pubkey_hash);
82        script.push(0x88); // OP_EQUALVERIFY
83        script.push(0xac); // OP_CHECKSIG
84        script
85    }
86
87    #[test]
88    fn p2pkh_stub_valid() {
89        let fake_sig = b"fake-signature";
90        let fake_pubkey = b"fake-public-key-data";
91        let pubkey_hash = hash::hash160(fake_pubkey);
92
93        let script_sig = build_script_sig(fake_sig, fake_pubkey);
94        let script_pubkey = build_script_pubkey(&pubkey_hash);
95
96        // Stub CHECKSIG always succeeds, so this should pass
97        let result = validate_p2pkh(&script_sig, &script_pubkey).unwrap();
98        assert!(result);
99    }
100
101    #[test]
102    fn p2pkh_wrong_pubkey_hash() {
103        let fake_sig = b"fake-signature";
104        let fake_pubkey = b"fake-public-key-data";
105        let wrong_hash = [0xab; 20]; // does not match hash160(fake_pubkey)
106
107        let script_sig = build_script_sig(fake_sig, fake_pubkey);
108        let script_pubkey = build_script_pubkey(&wrong_hash);
109
110        // OP_EQUALVERIFY should fail
111        let err = validate_p2pkh(&script_sig, &script_pubkey).unwrap_err();
112        assert!(matches!(err, ScriptError::VerifyFailed));
113    }
114
115    #[test]
116    fn p2pkh_empty_scriptsig() {
117        let pubkey_hash = [0x00; 20];
118        let script_pubkey = build_script_pubkey(&pubkey_hash);
119
120        // Empty scriptSig means stack is empty when scriptPubKey runs,
121        // OP_DUP will fail with StackUnderflow
122        let err = validate_p2pkh(&[], &script_pubkey).unwrap_err();
123        assert!(matches!(err, ScriptError::StackUnderflow));
124    }
125
126    #[test]
127    fn p2pkh_with_opts_stub() {
128        let fake_sig = b"sig";
129        let fake_pubkey = b"key";
130        let pubkey_hash = hash::hash160(fake_pubkey);
131
132        let script_sig = build_script_sig(fake_sig, fake_pubkey);
133        let script_pubkey = build_script_pubkey(&pubkey_hash);
134
135        let opts = ExecuteOpts { sighash: None };
136        let result = validate_p2pkh_with_opts(&script_sig, &script_pubkey, &opts).unwrap();
137        assert!(result);
138    }
139
140    #[test]
141    fn two_phase_isolation() {
142        // Verify that scriptSig cannot inject flow control.
143        // A scriptSig containing OP_RETURN should fail during phase 1.
144        let script_sig = vec![0x6a]; // OP_RETURN
145        let script_pubkey = vec![0x51]; // OP_1 (would be true)
146
147        let err = validate_p2pkh(&script_sig, &script_pubkey).unwrap_err();
148        assert!(matches!(err, ScriptError::OpReturnEncountered));
149    }
150}