Skip to main content

kaspa_txscript/
standard.rs

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