kaspa_txscript/
standard.rs

1use crate::{
2    opcodes::codes::{OpBlake2b, OpCheckSig, OpCheckSigECDSA, OpData32, OpData33, OpEqual},
3    script_builder::{ScriptBuilder, ScriptBuilderResult},
4    script_class::ScriptClass,
5};
6use blake2b_simd::Params;
7use kaspa_addresses::{Address, Prefix, Version};
8use kaspa_consensus_core::tx::{ScriptPublicKey, ScriptVec};
9use kaspa_txscript_errors::TxScriptError;
10use smallvec::SmallVec;
11use std::iter::once;
12
13mod multisig;
14
15pub use multisig::{multisig_redeem_script, multisig_redeem_script_ecdsa, Error as MultisigCreateError};
16
17/// Creates a new script to pay a transaction output to a 32-byte pubkey.
18fn pay_to_pub_key(address_payload: &[u8]) -> ScriptVec {
19    // TODO: use ScriptBuilder when add_op and add_data fns or equivalents are available
20    assert_eq!(address_payload.len(), 32);
21    SmallVec::from_iter(once(OpData32).chain(address_payload.iter().copied()).chain(once(OpCheckSig)))
22}
23
24/// Creates a new script to pay a transaction output to a 33-byte ECDSA pubkey.
25fn pay_to_pub_key_ecdsa(address_payload: &[u8]) -> ScriptVec {
26    // TODO: use ScriptBuilder when add_op and add_data fns or equivalents are available
27    assert_eq!(address_payload.len(), 33);
28    SmallVec::from_iter(once(OpData33).chain(address_payload.iter().copied()).chain(once(OpCheckSigECDSA)))
29}
30
31/// Creates a new script to pay a transaction output to a script hash.
32/// It is expected that the input is a valid hash.
33fn pay_to_script_hash(script_hash: &[u8]) -> ScriptVec {
34    // TODO: use ScriptBuilder when add_op and add_data fns or equivalents are available
35    assert_eq!(script_hash.len(), 32);
36    SmallVec::from_iter([OpBlake2b, OpData32].iter().copied().chain(script_hash.iter().copied()).chain(once(OpEqual)))
37}
38
39/// Creates a new script to pay a transaction output to the specified address.
40pub fn pay_to_address_script(address: &Address) -> ScriptPublicKey {
41    let script = match address.version {
42        Version::PubKey => pay_to_pub_key(address.payload.as_slice()),
43        Version::PubKeyECDSA => pay_to_pub_key_ecdsa(address.payload.as_slice()),
44        Version::ScriptHash => pay_to_script_hash(address.payload.as_slice()),
45    };
46    ScriptPublicKey::new(ScriptClass::from(address.version).version(), script)
47}
48
49/// Takes a script and returns an equivalent pay-to-script-hash script
50pub fn pay_to_script_hash_script(redeem_script: &[u8]) -> ScriptPublicKey {
51    let redeem_script_hash = Params::new().hash_length(32).to_state().update(redeem_script).finalize();
52    let script = pay_to_script_hash(redeem_script_hash.as_bytes());
53    ScriptPublicKey::new(ScriptClass::ScriptHash.version(), script)
54}
55
56/// Generates a signature script that fits a pay-to-script-hash script
57pub fn pay_to_script_hash_signature_script(redeem_script: Vec<u8>, signature: Vec<u8>) -> ScriptBuilderResult<Vec<u8>> {
58    let redeem_script_as_data = ScriptBuilder::new().add_data(&redeem_script)?.drain();
59    Ok(Vec::from_iter(signature.iter().copied().chain(redeem_script_as_data.iter().copied())))
60}
61
62/// Returns the address encoded in a script public key.
63///
64/// Notes:
65///  - This function only works for 'standard' transaction script types.
66///    Any data such as public keys which are invalid will return the
67///    `TxScriptError::PubKeyFormat` error.
68///
69///  - In case a ScriptClass is needed by the caller, call `ScriptClass::from(address.version)`
70///    or use `address.version` directly instead, where address is the successfully
71///    returned address.
72pub fn extract_script_pub_key_address(script_public_key: &ScriptPublicKey, prefix: Prefix) -> Result<Address, TxScriptError> {
73    let class = ScriptClass::from_script(script_public_key);
74    if script_public_key.version() > class.version() {
75        return Err(TxScriptError::PubKeyFormat);
76    }
77    let script = script_public_key.script();
78    match class {
79        ScriptClass::NonStandard => Err(TxScriptError::PubKeyFormat),
80        ScriptClass::PubKey => Ok(Address::new(prefix, Version::PubKey, &script[1..33])),
81        ScriptClass::PubKeyECDSA => Ok(Address::new(prefix, Version::PubKeyECDSA, &script[1..34])),
82        ScriptClass::ScriptHash => Ok(Address::new(prefix, Version::ScriptHash, &script[2..34])),
83    }
84}
85
86pub mod test_helpers {
87    use super::*;
88    use crate::{opcodes::codes::OpTrue, MAX_TX_IN_SEQUENCE_NUM};
89    use kaspa_consensus_core::{
90        constants::TX_VERSION,
91        subnets::SUBNETWORK_ID_NATIVE,
92        tx::{Transaction, TransactionInput, TransactionOutpoint, TransactionOutput},
93    };
94
95    /// Returns a P2SH script paying to an anyone-can-spend address,
96    /// The second return value is a redeemScript to be used with txscript.pay_to_script_hash_signature_script
97    pub fn op_true_script() -> (ScriptPublicKey, Vec<u8>) {
98        let redeem_script = vec![OpTrue];
99        let script_public_key = pay_to_script_hash_script(&redeem_script);
100        (script_public_key, redeem_script)
101    }
102
103    /// Creates a transaction that spends the first output of provided transaction.
104    /// Assumes that the output being spent has opTrueScript as its scriptPublicKey.
105    /// Creates the value of the spent output minus provided `fee` (in sompi).
106    pub fn create_transaction(tx_to_spend: &Transaction, fee: u64) -> Transaction {
107        let (script_public_key, redeem_script) = op_true_script();
108        let signature_script = pay_to_script_hash_signature_script(redeem_script, vec![]).expect("the script is canonical");
109        let previous_outpoint = TransactionOutpoint::new(tx_to_spend.id(), 0);
110        let input = TransactionInput::new(previous_outpoint, signature_script, MAX_TX_IN_SEQUENCE_NUM, 1);
111        let output = TransactionOutput::new(tx_to_spend.outputs[0].value - fee, script_public_key);
112        Transaction::new(TX_VERSION, vec![input], vec![output], 0, SUBNETWORK_ID_NATIVE, 0, vec![])
113    }
114
115    /// Creates a transaction that spends the outputs of specified indexes (if they exist) of every provided transaction and returns an optional change.
116    /// Assumes that the outputs being spent have opTrueScript as their scriptPublicKey.
117    ///
118    /// If some change is provided, creates two outputs, first one with the value of the spent outputs minus `change`
119    /// and `fee` (in sompi) and second one of `change` amount.
120    ///
121    /// If no change is provided, creates only one output with the value of the spent outputs minus and `fee` (in sompi)
122    pub fn create_transaction_with_change<'a>(
123        txs_to_spend: impl Iterator<Item = &'a Transaction>,
124        output_indexes: Vec<usize>,
125        change: Option<u64>,
126        fee: u64,
127    ) -> Transaction {
128        let (script_public_key, redeem_script) = op_true_script();
129        let signature_script = pay_to_script_hash_signature_script(redeem_script, vec![]).expect("the script is canonical");
130        let mut inputs_value: u64 = 0;
131        let mut inputs = vec![];
132        for tx_to_spend in txs_to_spend {
133            for i in output_indexes.iter().copied() {
134                if i < tx_to_spend.outputs.len() {
135                    let previous_outpoint = TransactionOutpoint::new(tx_to_spend.id(), i as u32);
136                    inputs.push(TransactionInput::new(previous_outpoint, signature_script.clone(), MAX_TX_IN_SEQUENCE_NUM, 1));
137                    inputs_value += tx_to_spend.outputs[i].value;
138                }
139            }
140        }
141        let outputs = match change {
142            Some(change) => vec![
143                TransactionOutput::new(inputs_value - fee - change, script_public_key.clone()),
144                TransactionOutput::new(change, script_public_key),
145            ],
146            None => vec![TransactionOutput::new(inputs_value - fee, script_public_key.clone())],
147        };
148        Transaction::new(TX_VERSION, inputs, outputs, 0, SUBNETWORK_ID_NATIVE, 0, vec![])
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_extract_address_and_encode_script() {
158        struct Test {
159            name: &'static str,
160            script_pub_key: ScriptPublicKey,
161            prefix: Prefix,
162            expected_address: Result<Address, TxScriptError>,
163        }
164
165        // cspell:disable
166        let tests = vec![
167            Test {
168                name: "Mainnet PubKey script and address",
169                script_pub_key: ScriptPublicKey::new(
170                    ScriptClass::PubKey.version(),
171                    ScriptVec::from_slice(
172                        &hex::decode("207bc04196f1125e4f2676cd09ed14afb77223b1f62177da5488346323eaa91a69ac").unwrap(),
173                    ),
174                ),
175                prefix: Prefix::Mainnet,
176                expected_address: Ok("kaspa:qpauqsvk7yf9unexwmxsnmg547mhyga37csh0kj53q6xxgl24ydxjsgzthw5j".try_into().unwrap()),
177            },
178            Test {
179                name: "Testnet PubKeyECDSA script and address",
180                script_pub_key: ScriptPublicKey::new(
181                    ScriptClass::PubKeyECDSA.version(),
182                    ScriptVec::from_slice(
183                        &hex::decode("21ba01fc5f4e9d9879599c69a3dafdb835a7255e5f2e934e9322ecd3af190ab0f60eab").unwrap(),
184                    ),
185                ),
186                prefix: Prefix::Testnet,
187                expected_address: Ok("kaspatest:qxaqrlzlf6wes72en3568khahq66wf27tuhfxn5nytkd8tcep2c0vrse6gdmpks".try_into().unwrap()),
188            },
189            Test {
190                name: "Testnet non standard script",
191                script_pub_key: ScriptPublicKey::new(
192                    ScriptClass::PubKey.version(),
193                    ScriptVec::from_slice(
194                        &hex::decode("2001fc5f4e9d9879599c69a3dafdb835a7255e5f2e934e9322ecd3af190ab0f60eab").unwrap(),
195                    ),
196                ),
197                prefix: Prefix::Testnet,
198                expected_address: Err(TxScriptError::PubKeyFormat),
199            },
200            Test {
201                name: "Mainnet script with unknown version",
202                script_pub_key: ScriptPublicKey::new(
203                    ScriptClass::PubKey.version() + 1,
204                    ScriptVec::from_slice(
205                        &hex::decode("207bc04196f1125e4f2676cd09ed14afb77223b1f62177da5488346323eaa91a69ac").unwrap(),
206                    ),
207                ),
208                prefix: Prefix::Mainnet,
209                expected_address: Err(TxScriptError::PubKeyFormat),
210            },
211        ];
212        // cspell:enable
213
214        for test in tests {
215            let extracted = extract_script_pub_key_address(&test.script_pub_key, test.prefix);
216            assert_eq!(extracted, test.expected_address, "extract address test failed for '{}'", test.name);
217            if let Ok(ref address) = extracted {
218                let encoded = pay_to_address_script(address);
219                assert_eq!(encoded, test.script_pub_key, "encode public key script test failed for '{}'", test.name);
220            }
221        }
222    }
223}