kaspa_consensus_core/
sign.rs

1use crate::{
2    hashing::{
3        sighash::{calc_schnorr_signature_hash, SigHashReusedValues},
4        sighash_type::{SigHashType, SIG_HASH_ALL},
5    },
6    tx::{SignableTransaction, VerifiableTransaction},
7};
8use itertools::Itertools;
9use std::collections::BTreeMap;
10use std::iter::once;
11use thiserror::Error;
12
13#[derive(Error, Debug, Clone)]
14pub enum Error {
15    #[error("{0}")]
16    Message(String),
17
18    #[error("Secp256k1 -> {0}")]
19    Secp256k1Error(#[from] secp256k1::Error),
20
21    #[error("The transaction is partially signed")]
22    PartiallySigned,
23
24    #[error("The transaction is fully signed")]
25    FullySigned,
26}
27
28/// A wrapper enum that represents the transaction signed state. A transaction
29/// contained by this enum can be either fully signed or partially signed.
30pub enum Signed {
31    Fully(SignableTransaction),
32    Partially(SignableTransaction),
33}
34
35impl Signed {
36    /// Returns the transaction if it is fully signed, otherwise returns an error
37    pub fn fully_signed(self) -> std::result::Result<SignableTransaction, Error> {
38        match self {
39            Signed::Fully(tx) => Ok(tx),
40            Signed::Partially(_) => Err(Error::PartiallySigned),
41        }
42    }
43
44    /// Returns the transaction if it is fully signed, otherwise returns the
45    /// transaction as an error `Err(tx)`.
46    #[allow(clippy::result_large_err)]
47    pub fn try_fully_signed(self) -> std::result::Result<SignableTransaction, SignableTransaction> {
48        match self {
49            Signed::Fully(tx) => Ok(tx),
50            Signed::Partially(tx) => Err(tx),
51        }
52    }
53
54    /// Returns the transaction if it is partially signed, otherwise fail with an error
55    pub fn partially_signed(self) -> std::result::Result<SignableTransaction, Error> {
56        match self {
57            Signed::Fully(_) => Err(Error::FullySigned),
58            Signed::Partially(tx) => Ok(tx),
59        }
60    }
61
62    /// Returns the transaction if it is partially signed, otherwise returns the
63    /// transaction as an error `Err(tx)`.
64    #[allow(clippy::result_large_err)]
65    pub fn try_partially_signed(self) -> std::result::Result<SignableTransaction, SignableTransaction> {
66        match self {
67            Signed::Fully(tx) => Err(tx),
68            Signed::Partially(tx) => Ok(tx),
69        }
70    }
71
72    /// Returns the transaction regardless of whether it is fully or partially signed
73    pub fn unwrap(self) -> SignableTransaction {
74        match self {
75            Signed::Fully(tx) => tx,
76            Signed::Partially(tx) => tx,
77        }
78    }
79}
80
81/// Sign a transaction using schnorr
82pub fn sign(mut signable_tx: SignableTransaction, schnorr_key: secp256k1::Keypair) -> SignableTransaction {
83    for i in 0..signable_tx.tx.inputs.len() {
84        signable_tx.tx.inputs[i].sig_op_count = 1;
85    }
86
87    let mut reused_values = SigHashReusedValues::new();
88    for i in 0..signable_tx.tx.inputs.len() {
89        let sig_hash = calc_schnorr_signature_hash(&signable_tx.as_verifiable(), i, SIG_HASH_ALL, &mut reused_values);
90        let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
91        let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref();
92        // This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
93        signable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect();
94    }
95    signable_tx
96}
97
98/// Sign a transaction using schnorr
99pub fn sign_with_multiple(mut mutable_tx: SignableTransaction, privkeys: Vec<[u8; 32]>) -> SignableTransaction {
100    let mut map = BTreeMap::new();
101    for privkey in privkeys {
102        let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &privkey).unwrap();
103        map.insert(schnorr_key.public_key().serialize(), schnorr_key);
104    }
105    for i in 0..mutable_tx.tx.inputs.len() {
106        mutable_tx.tx.inputs[i].sig_op_count = 1;
107    }
108
109    let mut reused_values = SigHashReusedValues::new();
110    for i in 0..mutable_tx.tx.inputs.len() {
111        let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script();
112        if let Some(schnorr_key) = map.get(script) {
113            let sig_hash = calc_schnorr_signature_hash(&mutable_tx.as_verifiable(), i, SIG_HASH_ALL, &mut reused_values);
114            let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
115            let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref();
116            // This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
117            mutable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect();
118        }
119    }
120    mutable_tx
121}
122
123/// TODO (aspect) - merge this with `v1` fn above or refactor wallet core to use the script engine.
124/// Sign a transaction using schnorr
125#[allow(clippy::result_large_err)]
126pub fn sign_with_multiple_v2(mut mutable_tx: SignableTransaction, privkeys: &[[u8; 32]]) -> Signed {
127    let mut map = BTreeMap::new();
128    for privkey in privkeys {
129        let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey).unwrap();
130        let schnorr_public_key = schnorr_key.public_key().x_only_public_key().0;
131        let script_pub_key_script = once(0x20).chain(schnorr_public_key.serialize().into_iter()).chain(once(0xac)).collect_vec();
132        map.insert(script_pub_key_script, schnorr_key);
133    }
134
135    let mut reused_values = SigHashReusedValues::new();
136    let mut additional_signatures_required = false;
137    for i in 0..mutable_tx.tx.inputs.len() {
138        let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script();
139        if let Some(schnorr_key) = map.get(script) {
140            let sig_hash = calc_schnorr_signature_hash(&mutable_tx.as_verifiable(), i, SIG_HASH_ALL, &mut reused_values);
141            let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
142            let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref();
143            // This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
144            mutable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig).chain([SIG_HASH_ALL.to_u8()]).collect();
145        } else {
146            additional_signatures_required = true;
147        }
148    }
149    if additional_signatures_required {
150        Signed::Partially(mutable_tx)
151    } else {
152        Signed::Fully(mutable_tx)
153    }
154}
155
156/// Sign a transaction input with a sighash_type using schnorr
157pub fn sign_input(tx: &impl VerifiableTransaction, input_index: usize, private_key: &[u8; 32], hash_type: SigHashType) -> Vec<u8> {
158    let mut reused_values = SigHashReusedValues::new();
159
160    let hash = calc_schnorr_signature_hash(tx, input_index, hash_type, &mut reused_values);
161    let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice()).unwrap();
162    let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, private_key).unwrap();
163    let sig: [u8; 64] = *schnorr_key.sign_schnorr(msg).as_ref();
164
165    // This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
166    std::iter::once(65u8).chain(sig).chain([hash_type.to_u8()]).collect()
167}
168
169pub fn verify(tx: &impl VerifiableTransaction) -> Result<(), Error> {
170    let mut reused_values = SigHashReusedValues::new();
171    for (i, (input, entry)) in tx.populated_inputs().enumerate() {
172        if input.signature_script.is_empty() {
173            return Err(Error::Message(format!("Signature is empty for input: {i}")));
174        }
175        let pk = &entry.script_public_key.script()[1..33];
176        let pk = secp256k1::XOnlyPublicKey::from_slice(pk)?;
177        let sig = secp256k1::schnorr::Signature::from_slice(&input.signature_script[1..65])?;
178        let sig_hash = calc_schnorr_signature_hash(tx, i, SIG_HASH_ALL, &mut reused_values);
179        let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice())?;
180        sig.verify(&msg, &pk)?;
181    }
182
183    Ok(())
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::{subnets::SubnetworkId, tx::*};
190    use secp256k1::{rand, Secp256k1};
191    use std::str::FromStr;
192
193    #[test]
194    fn test_and_verify_sign() {
195        let secp = Secp256k1::new();
196        let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng());
197        let script_pub_key = ScriptVec::from_slice(&public_key.serialize());
198
199        let (secret_key2, public_key2) = secp.generate_keypair(&mut rand::thread_rng());
200        let script_pub_key2 = ScriptVec::from_slice(&public_key2.serialize());
201
202        let prev_tx_id = TransactionId::from_str("880eb9819a31821d9d2399e2f35e2433b72637e393d71ecc9b8d0250f49153c3").unwrap();
203        let unsigned_tx = Transaction::new(
204            0,
205            vec![
206                TransactionInput {
207                    previous_outpoint: TransactionOutpoint { transaction_id: prev_tx_id, index: 0 },
208                    signature_script: vec![],
209                    sequence: 0,
210                    sig_op_count: 0,
211                },
212                TransactionInput {
213                    previous_outpoint: TransactionOutpoint { transaction_id: prev_tx_id, index: 1 },
214                    signature_script: vec![],
215                    sequence: 1,
216                    sig_op_count: 0,
217                },
218                TransactionInput {
219                    previous_outpoint: TransactionOutpoint { transaction_id: prev_tx_id, index: 2 },
220                    signature_script: vec![],
221                    sequence: 2,
222                    sig_op_count: 0,
223                },
224            ],
225            vec![
226                TransactionOutput { value: 300, script_public_key: ScriptPublicKey::new(0, script_pub_key.clone()) },
227                TransactionOutput { value: 300, script_public_key: ScriptPublicKey::new(0, script_pub_key.clone()) },
228            ],
229            1615462089000,
230            SubnetworkId::from_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
231            0,
232            vec![],
233        );
234
235        let entries = vec![
236            UtxoEntry {
237                amount: 100,
238                script_public_key: ScriptPublicKey::new(0, script_pub_key.clone()),
239                block_daa_score: 0,
240                is_coinbase: false,
241            },
242            UtxoEntry {
243                amount: 200,
244                script_public_key: ScriptPublicKey::new(0, script_pub_key),
245                block_daa_score: 0,
246                is_coinbase: false,
247            },
248            UtxoEntry {
249                amount: 300,
250                script_public_key: ScriptPublicKey::new(0, script_pub_key2),
251                block_daa_score: 0,
252                is_coinbase: false,
253            },
254        ];
255        let signed_tx = sign_with_multiple(
256            SignableTransaction::with_entries(unsigned_tx, entries),
257            vec![secret_key.secret_bytes(), secret_key2.secret_bytes()],
258        );
259
260        assert!(verify(&signed_tx.as_verifiable()).is_ok());
261    }
262}