Skip to main content

tidecoin_consensus_core/
tx.rs

1//! Transaction context and precomputed data for consensus validation.
2
3use alloc::vec::Vec;
4
5use hashes::{sha256, sha256d};
6use primitives::{Transaction, TxOut};
7
8use crate::{TidecoinValidationError, WitnessProgram};
9
10/// Borrows the transaction being verified.
11#[derive(Debug, Clone, Copy)]
12pub struct TransactionContext<'tx> {
13    tx: &'tx Transaction,
14}
15
16impl<'tx> TransactionContext<'tx> {
17    /// Creates a typed transaction validation context.
18    pub fn new(tx: &'tx Transaction) -> Self {
19        Self { tx }
20    }
21
22    /// Returns the borrowed transaction.
23    pub fn tx(&self) -> &'tx Transaction {
24        self.tx
25    }
26
27    /// Ensures `input_index` points to an existing transaction input.
28    pub fn ensure_input_index(&self, input_index: usize) -> Result<(), TidecoinValidationError> {
29        if input_index >= self.tx.inputs.len() {
30            Err(TidecoinValidationError::InvalidInputIndex {
31                index: input_index,
32                inputs: self.tx.inputs.len(),
33            })
34        } else {
35            Ok(())
36        }
37    }
38
39    /// Builds precomputed data required for signature hashing.
40    pub fn build_precomputed(
41        &self,
42        spent_outputs: Option<&SpentOutputs>,
43        force: bool,
44    ) -> PrecomputedTransactionData {
45        PrecomputedTransactionData::new(self.tx, spent_outputs, force)
46    }
47}
48
49/// Typed spent outputs referenced by a transaction during verification.
50#[derive(Debug, Clone)]
51pub struct SpentOutputs {
52    txouts: Vec<TxOut>,
53}
54
55impl SpentOutputs {
56    /// Creates a spent-output set from typed transaction outputs.
57    pub fn from_txouts(txouts: Vec<TxOut>) -> Self {
58        Self { txouts }
59    }
60
61    /// Returns the borrowed spent outputs.
62    pub fn txouts(&self) -> &[TxOut] {
63        &self.txouts
64    }
65}
66
67/// Precomputed transaction data for witness-aware verification.
68#[derive(Debug, Clone, Default)]
69pub struct PrecomputedTransactionData {
70    /// Single-SHA256 hash of all prevouts for Tidecoin witness paths.
71    pub prevouts_hash32: Option<sha256::Hash>,
72    /// Single-SHA256 hash of all input sequences for Tidecoin witness paths.
73    pub sequences_hash32: Option<sha256::Hash>,
74    /// Single-SHA256 hash of all outputs for Tidecoin witness paths.
75    pub outputs_hash32: Option<sha256::Hash>,
76    /// Single-SHA256 hash of spent-output amounts for `WITNESS_V1_512`.
77    pub spent_amounts_hash32: Option<sha256::Hash>,
78    /// Single-SHA256 hash of spent-output scripts for `WITNESS_V1_512`.
79    pub spent_scripts_hash32: Option<sha256::Hash>,
80    /// Double-SHA256 prevout hash for legacy and SegWit v0 sighash.
81    pub hash_prevouts: Option<sha256d::Hash>,
82    /// Double-SHA256 sequence hash for legacy and SegWit v0 sighash.
83    pub hash_sequence: Option<sha256d::Hash>,
84    /// Double-SHA256 output hash for legacy and SegWit v0 sighash.
85    pub hash_outputs: Option<sha256d::Hash>,
86    /// Whether SegWit v0 precomputation is available.
87    pub witness_v0_ready: bool,
88    /// Whether Tidecoin witness-v1-512 precomputation is available.
89    pub witness_v1_ready: bool,
90}
91
92impl PrecomputedTransactionData {
93    /// Precomputes the hashes needed by active Tidecoin sighash modes.
94    pub fn new(tx: &Transaction, spent_outputs: Option<&SpentOutputs>, force: bool) -> Self {
95        let mut data = Self::default();
96
97        let (mut uses_witness_v0, mut uses_witness_v1) = (force, force);
98
99        for (idx, input) in tx.inputs.iter().enumerate() {
100            if input.witness.is_empty() {
101                continue;
102            }
103
104            if let Some(spent) = spent_outputs {
105                if WitnessProgram::is_v1_512(spent.txouts[idx].script_pubkey.as_bytes()) {
106                    uses_witness_v1 = true;
107                } else {
108                    uses_witness_v0 = true;
109                }
110            } else {
111                uses_witness_v0 = true;
112            }
113
114            if uses_witness_v0 && uses_witness_v1 {
115                break;
116            }
117        }
118
119        if uses_witness_v0 || uses_witness_v1 {
120            let prevouts = hash_prevouts_single(tx);
121            let sequences = hash_sequences_single(tx);
122            let outputs = hash_outputs_single(tx);
123            data.prevouts_hash32 = Some(prevouts);
124            data.sequences_hash32 = Some(sequences);
125            data.outputs_hash32 = Some(outputs);
126        }
127
128        if uses_witness_v0 {
129            data.hash_prevouts = Some(hash_prevouts_double(tx));
130            data.hash_sequence = Some(hash_sequences_double(tx));
131            data.hash_outputs = Some(hash_outputs_double(tx));
132            data.witness_v0_ready = true;
133        }
134
135        if uses_witness_v1 {
136            if let Some(spent) = spent_outputs {
137                data.spent_amounts_hash32 = Some(hash_spent_amounts_single(spent));
138                data.spent_scripts_hash32 = Some(hash_spent_scripts_single(spent));
139                data.witness_v1_ready = true;
140            }
141        }
142
143        data
144    }
145}
146
147fn hash_prevouts_single(tx: &Transaction) -> sha256::Hash {
148    hash_serialized(tx.inputs.iter().map(|input| &input.previous_output))
149}
150
151fn hash_sequences_single(tx: &Transaction) -> sha256::Hash {
152    hash_serialized(tx.inputs.iter().map(|input| &input.sequence))
153}
154
155fn hash_outputs_single(tx: &Transaction) -> sha256::Hash {
156    hash_serialized(tx.outputs.iter())
157}
158
159fn hash_prevouts_double(tx: &Transaction) -> sha256d::Hash {
160    hash_serialized_double(tx.inputs.iter().map(|input| &input.previous_output))
161}
162
163fn hash_sequences_double(tx: &Transaction) -> sha256d::Hash {
164    hash_serialized_double(tx.inputs.iter().map(|input| &input.sequence))
165}
166
167fn hash_outputs_double(tx: &Transaction) -> sha256d::Hash {
168    hash_serialized_double(tx.outputs.iter())
169}
170
171fn hash_spent_amounts_single(spent: &SpentOutputs) -> sha256::Hash {
172    let mut engine = sha256::Hash::engine();
173    for txout in spent.txouts() {
174        io::encode_to_writer(&txout.amount, &mut engine)
175            .expect("hash engine writes are infallible");
176    }
177    sha256::Hash::from_engine(engine)
178}
179
180fn hash_spent_scripts_single(spent: &SpentOutputs) -> sha256::Hash {
181    hash_serialized(spent.txouts().iter().map(|txout| txout.script_pubkey.as_script()))
182}
183
184fn hash_serialized<'a, I, T>(items: I) -> sha256::Hash
185where
186    I: IntoIterator<Item = &'a T>,
187    T: encoding::Encodable + 'a + ?Sized,
188{
189    let mut engine = sha256::Hash::engine();
190    for item in items {
191        io::encode_to_writer(item, &mut engine).expect("hash engine writes are infallible");
192    }
193    sha256::Hash::from_engine(engine)
194}
195
196fn hash_serialized_double<'a, I, T>(items: I) -> sha256d::Hash
197where
198    I: IntoIterator<Item = &'a T>,
199    T: encoding::Encodable + 'a + ?Sized,
200{
201    let mut engine = sha256d::Hash::engine();
202    for item in items {
203        io::encode_to_writer(item, &mut engine).expect("hash engine writes are infallible");
204    }
205    sha256d::Hash::from_engine(engine)
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use alloc::vec;
212    use encoding::encode_to_vec;
213    use primitives::{
214        absolute::LockTime, transaction::Version, Amount, OutPoint, ScriptPubKeyBuf, Sequence,
215        TxIn, Txid, Witness,
216    };
217
218    #[test]
219    fn borrows_transaction_and_detects_hashes() {
220        let tx = Transaction {
221            version: Version::TWO,
222            lock_time: LockTime::ZERO,
223            inputs: vec![TxIn {
224                previous_output: OutPoint { txid: Txid::from_byte_array([1u8; 32]), vout: 0 },
225                script_sig: primitives::ScriptSigBuf::new(),
226                sequence: Sequence::MAX,
227                witness: Witness::new(),
228            }],
229            outputs: vec![TxOut {
230                amount: Amount::from_sat(42).expect("valid amount"),
231                script_pubkey: ScriptPubKeyBuf::new(),
232            }],
233        };
234
235        let _encoded = encode_to_vec(&tx);
236        let ctx = TransactionContext::new(&tx);
237
238        assert_eq!(ctx.tx().compute_txid(), tx.compute_txid());
239
240        let pre = ctx.build_precomputed(None, true);
241        assert!(pre.hash_prevouts.is_some());
242        assert!(pre.hash_sequence.is_some());
243        assert!(pre.hash_outputs.is_some());
244    }
245
246    #[test]
247    fn spent_outputs_from_txouts() {
248        let spent = SpentOutputs::from_txouts(vec![TxOut {
249            amount: Amount::from_sat(10).expect("valid amount"),
250            script_pubkey: ScriptPubKeyBuf::from_bytes([0x51u8; 34].to_vec()),
251        }]);
252        assert_eq!(spent.txouts().len(), 1);
253        assert_eq!(spent.txouts()[0].amount.to_sat(), 10);
254    }
255
256    #[test]
257    fn precomputed_marks_witness_v1_ready_when_prevouts_known() {
258        let witness_v1_script =
259            ScriptPubKeyBuf::from_bytes([vec![0x51, 64], vec![0u8; 64]].concat());
260
261        let tx = Transaction {
262            version: Version::TWO,
263            lock_time: LockTime::ZERO,
264            inputs: vec![TxIn {
265                previous_output: OutPoint { txid: Txid::from_byte_array([2u8; 32]), vout: 0 },
266                script_sig: primitives::ScriptSigBuf::new(),
267                sequence: Sequence::MAX,
268                witness: Witness::from(vec![vec![0x01]]),
269            }],
270            outputs: vec![TxOut {
271                amount: Amount::from_sat(100).expect("valid amount"),
272                script_pubkey: ScriptPubKeyBuf::new(),
273            }],
274        };
275
276        let spent = SpentOutputs::from_txouts(vec![TxOut {
277            amount: Amount::from_sat(50).expect("valid amount"),
278            script_pubkey: witness_v1_script,
279        }]);
280
281        let ctx = TransactionContext::new(&tx);
282        let pre = ctx.build_precomputed(Some(&spent), false);
283        assert!(pre.witness_v1_ready);
284        assert!(pre.spent_amounts_hash32.is_some());
285        assert!(pre.spent_scripts_hash32.is_some());
286        assert!(!pre.witness_v0_ready);
287    }
288}