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 PoolCreate { token: Hash32, block_amt: u64, token_amt: u64, gas_limit: u64 },
97 PoolAdd { token: Hash32, block_amt: u64, token_max: u64, gas_limit: u64 },
100 PoolRemove { token: Hash32, shares: u64, gas_limit: u64 },
103 PoolSwapBuy { token: Hash32, block_in: u64, min_token_out: u64, gas_limit: u64 },
106 PoolSwapSell { token: Hash32, token_in: u64, min_block_out: u64, gas_limit: u64 },
109}
110
111impl ContractAction {
112 pub fn gas_limit(&self) -> u64 {
113 match self {
114 ContractAction::Deploy { gas_limit, .. } => *gas_limit,
115 ContractAction::Call { gas_limit, .. } => *gas_limit,
116 ContractAction::PoolCreate { gas_limit, .. } => *gas_limit,
117 ContractAction::PoolAdd { gas_limit, .. } => *gas_limit,
118 ContractAction::PoolRemove { gas_limit, .. } => *gas_limit,
119 ContractAction::PoolSwapBuy { gas_limit, .. } => *gas_limit,
120 ContractAction::PoolSwapSell { gas_limit, .. } => *gas_limit,
121 }
122 }
123
124 pub fn value(&self) -> u64 {
126 match self {
127 ContractAction::Deploy { .. } => 0,
128 ContractAction::Call { value, .. } => *value,
129 ContractAction::PoolCreate { block_amt, .. } => *block_amt,
130 ContractAction::PoolAdd { block_amt, .. } => *block_amt,
131 ContractAction::PoolRemove { .. } => 0,
132 ContractAction::PoolSwapBuy { block_in, .. } => *block_in,
133 ContractAction::PoolSwapSell { .. } => 0,
134 }
135 }
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
139pub struct Transaction {
140 pub version: u32,
141 pub inputs: Vec<TxInput>,
143 pub outputs: Vec<TxOutput>,
144 pub coinbase_data: Vec<u8>,
147 pub shielded: Option<ShieldedBundle>,
148 #[serde(default)]
150 pub contract: Option<ContractAction>,
151}
152
153impl Transaction {
154 pub fn is_coinbase(&self) -> bool {
157 self.inputs.is_empty() && self.shielded.is_none()
158 }
159
160 pub fn shielded_in(&self) -> Option<u64> {
162 self.shielded.as_ref().map_or(Some(0), |b| {
163 b.spends.iter().try_fold(0u64, |acc, s| acc.checked_add(s.value))
164 })
165 }
166
167 pub fn shielded_out(&self) -> Option<u64> {
169 self.shielded.as_ref().map_or(Some(0), |b| {
170 b.outputs.iter().try_fold(0u64, |acc, o| acc.checked_add(o.value))
171 })
172 }
173
174 pub fn transfer_fees(&self) -> Option<u64> {
176 self.shielded.as_ref().map_or(Some(0), |b| {
177 b.transfers.iter().try_fold(0u64, |acc, t| acc.checked_add(t.fee))
178 })
179 }
180
181 pub fn nullifiers(&self) -> Vec<Hash32> {
183 self.shielded
184 .as_ref()
185 .map(|b| {
186 b.spends
187 .iter()
188 .map(|s| s.nullifier)
189 .chain(b.transfers.iter().map(|t| t.nullifier))
190 .collect()
191 })
192 .unwrap_or_default()
193 }
194
195 pub fn encode(&self, for_sighash: bool) -> Vec<u8> {
198 let mut out = Vec::new();
199 put_u32(&mut out, self.version);
200 put_u32(&mut out, self.inputs.len() as u32);
201 for input in &self.inputs {
202 put_fixed(&mut out, &input.prev.txid);
203 put_u32(&mut out, input.prev.vout);
204 put_bytes(&mut out, &input.pubkey);
205 if for_sighash {
206 put_bytes(&mut out, &[]);
207 } else {
208 put_bytes(&mut out, &input.signature);
209 }
210 }
211 put_u32(&mut out, self.outputs.len() as u32);
212 for output in &self.outputs {
213 put_fixed(&mut out, &output.recipient);
214 put_u64(&mut out, output.amount);
215 }
216 put_bytes(&mut out, &self.coinbase_data);
217 match &self.shielded {
218 None => put_u32(&mut out, 0),
219 Some(s) => {
220 put_u32(&mut out, 1);
221 put_u32(&mut out, s.spends.len() as u32);
222 for spend in &s.spends {
223 put_fixed(&mut out, &spend.anchor);
224 put_fixed(&mut out, &spend.nullifier);
225 put_u64(&mut out, spend.value);
226 if for_sighash {
229 put_bytes(&mut out, &[]);
230 } else {
231 put_bytes(&mut out, &spend.proof);
232 }
233 }
234 put_u32(&mut out, s.outputs.len() as u32);
235 for output in &s.outputs {
236 put_fixed(&mut out, &output.commitment);
237 put_u64(&mut out, output.value);
238 }
239 put_u32(&mut out, s.transfers.len() as u32);
240 for t in &s.transfers {
241 put_fixed(&mut out, &t.anchor);
242 put_fixed(&mut out, &t.nullifier);
243 put_fixed(&mut out, &t.commitment1);
244 put_fixed(&mut out, &t.commitment2);
245 put_u64(&mut out, t.fee);
246 put_bytes(&mut out, &t.memo);
247 if for_sighash {
248 put_bytes(&mut out, &[]);
249 } else {
250 put_bytes(&mut out, &t.proof);
251 }
252 }
253 }
254 }
255 match &self.contract {
256 None => put_u32(&mut out, 0),
257 Some(ContractAction::Deploy { code, gas_limit }) => {
258 put_u32(&mut out, 1);
259 put_bytes(&mut out, code);
260 put_u64(&mut out, *gas_limit);
261 }
262 Some(ContractAction::Call { contract, input, value, gas_limit }) => {
263 put_u32(&mut out, 2);
264 put_fixed(&mut out, contract);
265 put_bytes(&mut out, input);
266 put_u64(&mut out, *value);
267 put_u64(&mut out, *gas_limit);
268 }
269 Some(ContractAction::PoolCreate { token, block_amt, token_amt, gas_limit }) => {
270 put_u32(&mut out, 3);
271 put_fixed(&mut out, token);
272 put_u64(&mut out, *block_amt);
273 put_u64(&mut out, *token_amt);
274 put_u64(&mut out, *gas_limit);
275 }
276 Some(ContractAction::PoolAdd { token, block_amt, token_max, gas_limit }) => {
277 put_u32(&mut out, 4);
278 put_fixed(&mut out, token);
279 put_u64(&mut out, *block_amt);
280 put_u64(&mut out, *token_max);
281 put_u64(&mut out, *gas_limit);
282 }
283 Some(ContractAction::PoolRemove { token, shares, gas_limit }) => {
284 put_u32(&mut out, 5);
285 put_fixed(&mut out, token);
286 put_u64(&mut out, *shares);
287 put_u64(&mut out, *gas_limit);
288 }
289 Some(ContractAction::PoolSwapBuy { token, block_in, min_token_out, gas_limit }) => {
290 put_u32(&mut out, 6);
291 put_fixed(&mut out, token);
292 put_u64(&mut out, *block_in);
293 put_u64(&mut out, *min_token_out);
294 put_u64(&mut out, *gas_limit);
295 }
296 Some(ContractAction::PoolSwapSell { token, token_in, min_block_out, gas_limit }) => {
297 put_u32(&mut out, 7);
298 put_fixed(&mut out, token);
299 put_u64(&mut out, *token_in);
300 put_u64(&mut out, *min_block_out);
301 put_u64(&mut out, *gas_limit);
302 }
303 }
304 out
305 }
306
307 pub fn txid(&self) -> Hash32 {
308 blake2b_256(&self.encode(false))
309 }
310
311 pub fn sighash(&self) -> Hash32 {
313 blake2b_256(&self.encode(true))
314 }
315
316 pub fn total_output(&self) -> Option<u64> {
317 self.outputs
318 .iter()
319 .try_fold(0u64, |acc, o| acc.checked_add(o.amount))
320 }
321
322 pub fn serialized_size(&self) -> usize {
323 self.encode(false).len()
324 }
325
326 pub fn coinbase(height: u64, recipient: Address, amount: u64, tag: &[u8]) -> Self {
328 let mut coinbase_data = height.to_le_bytes().to_vec();
329 coinbase_data.extend_from_slice(tag);
330 Transaction {
331 version: 1,
332 inputs: vec![],
333 outputs: vec![TxOutput { recipient, amount }],
334 coinbase_data,
335 shielded: None,
336 contract: None,
337 }
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::keys::Keypair;
345
346 #[test]
347 fn txid_changes_with_content_sighash_ignores_sigs() {
348 let kp = Keypair::generate();
349 let mut tx = Transaction {
350 version: 1,
351 inputs: vec![TxInput {
352 prev: OutPoint { txid: [1; 32], vout: 0 },
353 pubkey: kp.public_bytes(),
354 signature: vec![],
355 }],
356 outputs: vec![TxOutput { recipient: kp.address(), amount: 42 }],
357 coinbase_data: vec![],
358 shielded: None,
359 contract: None,
360 };
361 let sighash_before = tx.sighash();
362 let txid_before = tx.txid();
363 tx.inputs[0].signature = kp.sign(&sighash_before).to_vec();
364 assert_eq!(tx.sighash(), sighash_before, "sighash must ignore signatures");
365 assert_ne!(tx.txid(), txid_before, "txid commits to signatures");
366
367 tx.outputs[0].amount = 43;
368 assert_ne!(tx.sighash(), sighash_before);
369 }
370
371 #[test]
372 fn coinbase_txids_unique_per_height() {
373 let addr = [9u8; 32];
374 let a = Transaction::coinbase(1, addr, 50, b"blockle");
375 let b = Transaction::coinbase(2, addr, 50, b"blockle");
376 assert!(a.is_coinbase());
377 assert_ne!(a.txid(), b.txid());
378 }
379}