1use serde::{Deserialize, Serialize};
6
7use crate::encode::{put_bytes, put_fixed, put_u32, put_u64};
8use crate::hash::{blake2b_256, Hash32};
9use crate::keys::Address;
10
11#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
13pub struct OutPoint {
14 pub txid: Hash32,
15 pub vout: u32,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
19pub struct TxInput {
20 pub prev: OutPoint,
21 pub pubkey: Vec<u8>,
23 pub signature: Vec<u8>,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
28pub struct TxOutput {
29 pub recipient: Address,
30 pub amount: u64,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
40pub struct ShieldedSpend {
41 pub anchor: Hash32,
43 pub nullifier: Hash32,
44 pub value: u64,
45 pub proof: Vec<u8>,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
52pub struct ShieldedOutput {
53 pub commitment: Hash32,
54 pub value: u64,
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
62pub struct ShieldedTransfer {
63 pub anchor: Hash32,
64 pub nullifier: Hash32,
65 pub commitment1: Hash32,
66 pub commitment2: Hash32,
67 pub fee: u64,
68 pub memo: Vec<u8>,
69 pub proof: Vec<u8>,
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
73pub struct ShieldedBundle {
74 pub spends: Vec<ShieldedSpend>,
75 pub outputs: Vec<ShieldedOutput>,
76 #[serde(default)]
78 pub transfers: Vec<ShieldedTransfer>,
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
85pub enum ContractAction {
86 Deploy { code: Vec<u8>, gas_limit: u64 },
89 Call { contract: Hash32, input: Vec<u8>, value: u64, gas_limit: u64 },
93}
94
95impl ContractAction {
96 pub fn gas_limit(&self) -> u64 {
97 match self {
98 ContractAction::Deploy { gas_limit, .. } => *gas_limit,
99 ContractAction::Call { gas_limit, .. } => *gas_limit,
100 }
101 }
102
103 pub fn value(&self) -> u64 {
104 match self {
105 ContractAction::Deploy { .. } => 0,
106 ContractAction::Call { value, .. } => *value,
107 }
108 }
109}
110
111#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
112pub struct Transaction {
113 pub version: u32,
114 pub inputs: Vec<TxInput>,
116 pub outputs: Vec<TxOutput>,
117 pub coinbase_data: Vec<u8>,
120 pub shielded: Option<ShieldedBundle>,
121 #[serde(default)]
123 pub contract: Option<ContractAction>,
124}
125
126impl Transaction {
127 pub fn is_coinbase(&self) -> bool {
130 self.inputs.is_empty() && self.shielded.is_none()
131 }
132
133 pub fn shielded_in(&self) -> Option<u64> {
135 self.shielded.as_ref().map_or(Some(0), |b| {
136 b.spends.iter().try_fold(0u64, |acc, s| acc.checked_add(s.value))
137 })
138 }
139
140 pub fn shielded_out(&self) -> Option<u64> {
142 self.shielded.as_ref().map_or(Some(0), |b| {
143 b.outputs.iter().try_fold(0u64, |acc, o| acc.checked_add(o.value))
144 })
145 }
146
147 pub fn transfer_fees(&self) -> Option<u64> {
149 self.shielded.as_ref().map_or(Some(0), |b| {
150 b.transfers.iter().try_fold(0u64, |acc, t| acc.checked_add(t.fee))
151 })
152 }
153
154 pub fn nullifiers(&self) -> Vec<Hash32> {
156 self.shielded
157 .as_ref()
158 .map(|b| {
159 b.spends
160 .iter()
161 .map(|s| s.nullifier)
162 .chain(b.transfers.iter().map(|t| t.nullifier))
163 .collect()
164 })
165 .unwrap_or_default()
166 }
167
168 pub fn encode(&self, for_sighash: bool) -> Vec<u8> {
171 let mut out = Vec::new();
172 put_u32(&mut out, self.version);
173 put_u32(&mut out, self.inputs.len() as u32);
174 for input in &self.inputs {
175 put_fixed(&mut out, &input.prev.txid);
176 put_u32(&mut out, input.prev.vout);
177 put_bytes(&mut out, &input.pubkey);
178 if for_sighash {
179 put_bytes(&mut out, &[]);
180 } else {
181 put_bytes(&mut out, &input.signature);
182 }
183 }
184 put_u32(&mut out, self.outputs.len() as u32);
185 for output in &self.outputs {
186 put_fixed(&mut out, &output.recipient);
187 put_u64(&mut out, output.amount);
188 }
189 put_bytes(&mut out, &self.coinbase_data);
190 match &self.shielded {
191 None => put_u32(&mut out, 0),
192 Some(s) => {
193 put_u32(&mut out, 1);
194 put_u32(&mut out, s.spends.len() as u32);
195 for spend in &s.spends {
196 put_fixed(&mut out, &spend.anchor);
197 put_fixed(&mut out, &spend.nullifier);
198 put_u64(&mut out, spend.value);
199 if for_sighash {
202 put_bytes(&mut out, &[]);
203 } else {
204 put_bytes(&mut out, &spend.proof);
205 }
206 }
207 put_u32(&mut out, s.outputs.len() as u32);
208 for output in &s.outputs {
209 put_fixed(&mut out, &output.commitment);
210 put_u64(&mut out, output.value);
211 }
212 put_u32(&mut out, s.transfers.len() as u32);
213 for t in &s.transfers {
214 put_fixed(&mut out, &t.anchor);
215 put_fixed(&mut out, &t.nullifier);
216 put_fixed(&mut out, &t.commitment1);
217 put_fixed(&mut out, &t.commitment2);
218 put_u64(&mut out, t.fee);
219 put_bytes(&mut out, &t.memo);
220 if for_sighash {
221 put_bytes(&mut out, &[]);
222 } else {
223 put_bytes(&mut out, &t.proof);
224 }
225 }
226 }
227 }
228 match &self.contract {
229 None => put_u32(&mut out, 0),
230 Some(ContractAction::Deploy { code, gas_limit }) => {
231 put_u32(&mut out, 1);
232 put_bytes(&mut out, code);
233 put_u64(&mut out, *gas_limit);
234 }
235 Some(ContractAction::Call { contract, input, value, gas_limit }) => {
236 put_u32(&mut out, 2);
237 put_fixed(&mut out, contract);
238 put_bytes(&mut out, input);
239 put_u64(&mut out, *value);
240 put_u64(&mut out, *gas_limit);
241 }
242 }
243 out
244 }
245
246 pub fn txid(&self) -> Hash32 {
247 blake2b_256(&self.encode(false))
248 }
249
250 pub fn sighash(&self) -> Hash32 {
252 blake2b_256(&self.encode(true))
253 }
254
255 pub fn total_output(&self) -> Option<u64> {
256 self.outputs
257 .iter()
258 .try_fold(0u64, |acc, o| acc.checked_add(o.amount))
259 }
260
261 pub fn serialized_size(&self) -> usize {
262 self.encode(false).len()
263 }
264
265 pub fn coinbase(height: u64, recipient: Address, amount: u64, tag: &[u8]) -> Self {
267 let mut coinbase_data = height.to_le_bytes().to_vec();
268 coinbase_data.extend_from_slice(tag);
269 Transaction {
270 version: 1,
271 inputs: vec![],
272 outputs: vec![TxOutput { recipient, amount }],
273 coinbase_data,
274 shielded: None,
275 contract: None,
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::keys::Keypair;
284
285 #[test]
286 fn txid_changes_with_content_sighash_ignores_sigs() {
287 let kp = Keypair::generate();
288 let mut tx = Transaction {
289 version: 1,
290 inputs: vec![TxInput {
291 prev: OutPoint { txid: [1; 32], vout: 0 },
292 pubkey: kp.public_bytes(),
293 signature: vec![],
294 }],
295 outputs: vec![TxOutput { recipient: kp.address(), amount: 42 }],
296 coinbase_data: vec![],
297 shielded: None,
298 contract: None,
299 };
300 let sighash_before = tx.sighash();
301 let txid_before = tx.txid();
302 tx.inputs[0].signature = kp.sign(&sighash_before).to_vec();
303 assert_eq!(tx.sighash(), sighash_before, "sighash must ignore signatures");
304 assert_ne!(tx.txid(), txid_before, "txid commits to signatures");
305
306 tx.outputs[0].amount = 43;
307 assert_ne!(tx.sighash(), sighash_before);
308 }
309
310 #[test]
311 fn coinbase_txids_unique_per_height() {
312 let addr = [9u8; 32];
313 let a = Transaction::coinbase(1, addr, 50, b"blockle");
314 let b = Transaction::coinbase(2, addr, 50, b"blockle");
315 assert!(a.is_coinbase());
316 assert_ne!(a.txid(), b.txid());
317 }
318}