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)]
90pub enum IntelAction {
91 SubnetRegister { schema: Vec<u8>, tier: u8, gas_limit: u64 },
94 NeuronRegister { subnet_id: u32, role: u8, hotkey: Vec<u8>, bond: u64, gas_limit: u64 },
98 SetWeightsCommit { subnet_id: u32, commit: [u8; 32], gas_limit: u64 },
100 SetWeightsReveal { subnet_id: u32, uids: Vec<u32>, weights: Vec<u32>, nonce: [u8; 32], gas_limit: u64 },
104 NeuronClaim { gas_limit: u64 },
106 NeuronDeregister { subnet_id: u32, uid: u32, gas_limit: u64 },
112 ValidationProof { subnet_id: u32, beacon_ack: [u8; 32], gas_limit: u64 },
117}
118
119impl IntelAction {
120 pub fn gas_limit(&self) -> u64 {
121 match self {
122 IntelAction::SubnetRegister { gas_limit, .. } => *gas_limit,
123 IntelAction::NeuronRegister { gas_limit, .. } => *gas_limit,
124 IntelAction::SetWeightsCommit { gas_limit, .. } => *gas_limit,
125 IntelAction::SetWeightsReveal { gas_limit, .. } => *gas_limit,
126 IntelAction::NeuronClaim { gas_limit, .. } => *gas_limit,
127 IntelAction::ValidationProof { gas_limit, .. } => *gas_limit,
128 IntelAction::NeuronDeregister { gas_limit, .. } => *gas_limit,
129 }
130 }
131 pub fn value(&self) -> u64 {
134 match self {
135 IntelAction::NeuronRegister { bond, .. } => *bond,
136 _ => 0,
137 }
138 }
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
142pub enum ContractAction {
143 Deploy { code: Vec<u8>, gas_limit: u64 },
146 Call { contract: Hash32, input: Vec<u8>, value: u64, gas_limit: u64 },
150 PoolCreate { token: Hash32, block_amt: u64, token_amt: u64, gas_limit: u64 },
154 PoolAdd { token: Hash32, block_amt: u64, token_max: u64, gas_limit: u64 },
157 PoolRemove { token: Hash32, shares: u64, gas_limit: u64 },
160 PoolSwapBuy { token: Hash32, block_in: u64, min_token_out: u64, gas_limit: u64 },
163 PoolSwapSell { token: Hash32, token_in: u64, min_block_out: u64, gas_limit: u64 },
166 LaunchToken { code: Vec<u8>, block_amt: u64, token_amt: u64, gas_limit: u64 },
173 LaunchNft { code: Vec<u8>, gas_limit: u64 },
178 Intel(IntelAction),
180}
181
182impl ContractAction {
183 pub fn gas_limit(&self) -> u64 {
184 match self {
185 ContractAction::Deploy { gas_limit, .. } => *gas_limit,
186 ContractAction::Call { gas_limit, .. } => *gas_limit,
187 ContractAction::PoolCreate { gas_limit, .. } => *gas_limit,
188 ContractAction::PoolAdd { gas_limit, .. } => *gas_limit,
189 ContractAction::PoolRemove { gas_limit, .. } => *gas_limit,
190 ContractAction::PoolSwapBuy { gas_limit, .. } => *gas_limit,
191 ContractAction::PoolSwapSell { gas_limit, .. } => *gas_limit,
192 ContractAction::LaunchToken { gas_limit, .. } => *gas_limit,
193 ContractAction::LaunchNft { gas_limit, .. } => *gas_limit,
194 ContractAction::Intel(a) => a.gas_limit(),
195 }
196 }
197
198 pub fn value(&self) -> u64 {
200 match self {
201 ContractAction::Deploy { .. } => 0,
202 ContractAction::Call { value, .. } => *value,
203 ContractAction::PoolCreate { block_amt, .. } => *block_amt,
204 ContractAction::PoolAdd { block_amt, .. } => *block_amt,
205 ContractAction::PoolRemove { .. } => 0,
206 ContractAction::PoolSwapBuy { block_in, .. } => *block_in,
207 ContractAction::PoolSwapSell { .. } => 0,
208 ContractAction::LaunchToken { block_amt, .. } => *block_amt,
209 ContractAction::LaunchNft { .. } => 0,
210 ContractAction::Intel(a) => a.value(),
211 }
212 }
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
216pub struct Transaction {
217 pub version: u32,
218 pub inputs: Vec<TxInput>,
220 pub outputs: Vec<TxOutput>,
221 pub coinbase_data: Vec<u8>,
224 pub shielded: Option<ShieldedBundle>,
225 #[serde(default)]
227 pub contract: Option<ContractAction>,
228}
229
230impl Transaction {
231 pub fn is_coinbase(&self) -> bool {
234 self.inputs.is_empty() && self.shielded.is_none()
235 }
236
237 pub fn shielded_in(&self) -> Option<u64> {
239 self.shielded.as_ref().map_or(Some(0), |b| {
240 b.spends.iter().try_fold(0u64, |acc, s| acc.checked_add(s.value))
241 })
242 }
243
244 pub fn shielded_out(&self) -> Option<u64> {
246 self.shielded.as_ref().map_or(Some(0), |b| {
247 b.outputs.iter().try_fold(0u64, |acc, o| acc.checked_add(o.value))
248 })
249 }
250
251 pub fn transfer_fees(&self) -> Option<u64> {
253 self.shielded.as_ref().map_or(Some(0), |b| {
254 b.transfers.iter().try_fold(0u64, |acc, t| acc.checked_add(t.fee))
255 })
256 }
257
258 pub fn nullifiers(&self) -> Vec<Hash32> {
260 self.shielded
261 .as_ref()
262 .map(|b| {
263 b.spends
264 .iter()
265 .map(|s| s.nullifier)
266 .chain(b.transfers.iter().map(|t| t.nullifier))
267 .collect()
268 })
269 .unwrap_or_default()
270 }
271
272 pub fn encode(&self, for_sighash: bool) -> Vec<u8> {
275 let mut out = Vec::new();
276 put_u32(&mut out, self.version);
277 put_u32(&mut out, self.inputs.len() as u32);
278 for input in &self.inputs {
279 put_fixed(&mut out, &input.prev.txid);
280 put_u32(&mut out, input.prev.vout);
281 put_bytes(&mut out, &input.pubkey);
282 if for_sighash {
283 put_bytes(&mut out, &[]);
284 } else {
285 put_bytes(&mut out, &input.signature);
286 }
287 }
288 put_u32(&mut out, self.outputs.len() as u32);
289 for output in &self.outputs {
290 put_fixed(&mut out, &output.recipient);
291 put_u64(&mut out, output.amount);
292 }
293 put_bytes(&mut out, &self.coinbase_data);
294 match &self.shielded {
295 None => put_u32(&mut out, 0),
296 Some(s) => {
297 put_u32(&mut out, 1);
298 put_u32(&mut out, s.spends.len() as u32);
299 for spend in &s.spends {
300 put_fixed(&mut out, &spend.anchor);
301 put_fixed(&mut out, &spend.nullifier);
302 put_u64(&mut out, spend.value);
303 if for_sighash {
306 put_bytes(&mut out, &[]);
307 } else {
308 put_bytes(&mut out, &spend.proof);
309 }
310 }
311 put_u32(&mut out, s.outputs.len() as u32);
312 for output in &s.outputs {
313 put_fixed(&mut out, &output.commitment);
314 put_u64(&mut out, output.value);
315 }
316 put_u32(&mut out, s.transfers.len() as u32);
317 for t in &s.transfers {
318 put_fixed(&mut out, &t.anchor);
319 put_fixed(&mut out, &t.nullifier);
320 put_fixed(&mut out, &t.commitment1);
321 put_fixed(&mut out, &t.commitment2);
322 put_u64(&mut out, t.fee);
323 put_bytes(&mut out, &t.memo);
324 if for_sighash {
325 put_bytes(&mut out, &[]);
326 } else {
327 put_bytes(&mut out, &t.proof);
328 }
329 }
330 }
331 }
332 match &self.contract {
333 None => put_u32(&mut out, 0),
334 Some(ContractAction::Deploy { code, gas_limit }) => {
335 put_u32(&mut out, 1);
336 put_bytes(&mut out, code);
337 put_u64(&mut out, *gas_limit);
338 }
339 Some(ContractAction::Call { contract, input, value, gas_limit }) => {
340 put_u32(&mut out, 2);
341 put_fixed(&mut out, contract);
342 put_bytes(&mut out, input);
343 put_u64(&mut out, *value);
344 put_u64(&mut out, *gas_limit);
345 }
346 Some(ContractAction::PoolCreate { token, block_amt, token_amt, gas_limit }) => {
347 put_u32(&mut out, 3);
348 put_fixed(&mut out, token);
349 put_u64(&mut out, *block_amt);
350 put_u64(&mut out, *token_amt);
351 put_u64(&mut out, *gas_limit);
352 }
353 Some(ContractAction::PoolAdd { token, block_amt, token_max, gas_limit }) => {
354 put_u32(&mut out, 4);
355 put_fixed(&mut out, token);
356 put_u64(&mut out, *block_amt);
357 put_u64(&mut out, *token_max);
358 put_u64(&mut out, *gas_limit);
359 }
360 Some(ContractAction::PoolRemove { token, shares, gas_limit }) => {
361 put_u32(&mut out, 5);
362 put_fixed(&mut out, token);
363 put_u64(&mut out, *shares);
364 put_u64(&mut out, *gas_limit);
365 }
366 Some(ContractAction::PoolSwapBuy { token, block_in, min_token_out, gas_limit }) => {
367 put_u32(&mut out, 6);
368 put_fixed(&mut out, token);
369 put_u64(&mut out, *block_in);
370 put_u64(&mut out, *min_token_out);
371 put_u64(&mut out, *gas_limit);
372 }
373 Some(ContractAction::PoolSwapSell { token, token_in, min_block_out, gas_limit }) => {
374 put_u32(&mut out, 7);
375 put_fixed(&mut out, token);
376 put_u64(&mut out, *token_in);
377 put_u64(&mut out, *min_block_out);
378 put_u64(&mut out, *gas_limit);
379 }
380 Some(ContractAction::LaunchToken { code, block_amt, token_amt, gas_limit }) => {
381 put_u32(&mut out, 8);
382 put_bytes(&mut out, code);
383 put_u64(&mut out, *block_amt);
384 put_u64(&mut out, *token_amt);
385 put_u64(&mut out, *gas_limit);
386 }
387 Some(ContractAction::LaunchNft { code, gas_limit }) => {
388 put_u32(&mut out, 9);
389 put_bytes(&mut out, code);
390 put_u64(&mut out, *gas_limit);
391 }
392 Some(ContractAction::Intel(IntelAction::SubnetRegister { schema, tier, gas_limit })) => {
394 put_u32(&mut out, 10);
395 put_u32(&mut out, 0); put_bytes(&mut out, schema);
397 out.push(*tier);
398 put_u64(&mut out, *gas_limit);
399 }
400 Some(ContractAction::Intel(IntelAction::NeuronRegister { subnet_id, role, hotkey, bond, gas_limit })) => {
401 put_u32(&mut out, 10);
402 put_u32(&mut out, 1); put_u32(&mut out, *subnet_id);
404 out.push(*role);
405 put_bytes(&mut out, hotkey);
406 put_u64(&mut out, *bond);
407 put_u64(&mut out, *gas_limit);
408 }
409 Some(ContractAction::Intel(IntelAction::SetWeightsCommit { subnet_id, commit, gas_limit })) => {
410 put_u32(&mut out, 10);
411 put_u32(&mut out, 2); put_u32(&mut out, *subnet_id);
413 put_fixed(&mut out, commit);
414 put_u64(&mut out, *gas_limit);
415 }
416 Some(ContractAction::Intel(IntelAction::SetWeightsReveal { subnet_id, uids, weights, nonce, gas_limit })) => {
417 put_u32(&mut out, 10);
418 put_u32(&mut out, 3); put_u32(&mut out, *subnet_id);
420 put_u32(&mut out, uids.len() as u32);
421 for u in uids { put_u32(&mut out, *u); }
422 put_u32(&mut out, weights.len() as u32);
423 for w in weights { put_u32(&mut out, *w); }
424 put_fixed(&mut out, nonce);
425 put_u64(&mut out, *gas_limit);
426 }
427 Some(ContractAction::Intel(IntelAction::NeuronClaim { gas_limit })) => {
428 put_u32(&mut out, 10);
429 put_u32(&mut out, 4); put_u64(&mut out, *gas_limit);
431 }
432 Some(ContractAction::Intel(IntelAction::ValidationProof { subnet_id, beacon_ack, gas_limit })) => {
433 put_u32(&mut out, 10);
434 put_u32(&mut out, 5); put_u32(&mut out, *subnet_id);
436 put_fixed(&mut out, beacon_ack);
437 put_u64(&mut out, *gas_limit);
438 }
439 Some(ContractAction::Intel(IntelAction::NeuronDeregister { subnet_id, uid, gas_limit })) => {
440 put_u32(&mut out, 10);
441 put_u32(&mut out, 6); put_u32(&mut out, *subnet_id);
443 put_u32(&mut out, *uid);
444 put_u64(&mut out, *gas_limit);
445 }
446 }
447 out
448 }
449
450 pub fn txid(&self) -> Hash32 {
451 blake2b_256(&self.encode(false))
452 }
453
454 pub fn sighash(&self) -> Hash32 {
456 blake2b_256(&self.encode(true))
457 }
458
459 pub fn total_output(&self) -> Option<u64> {
460 self.outputs
461 .iter()
462 .try_fold(0u64, |acc, o| acc.checked_add(o.amount))
463 }
464
465 pub fn serialized_size(&self) -> usize {
466 self.encode(false).len()
467 }
468
469 pub fn coinbase(height: u64, recipient: Address, amount: u64, tag: &[u8]) -> Self {
471 let mut coinbase_data = height.to_le_bytes().to_vec();
472 coinbase_data.extend_from_slice(tag);
473 Transaction {
474 version: 1,
475 inputs: vec![],
476 outputs: vec![TxOutput { recipient, amount }],
477 coinbase_data,
478 shielded: None,
479 contract: None,
480 }
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use crate::keys::Keypair;
488
489 #[test]
490 fn txid_changes_with_content_sighash_ignores_sigs() {
491 let kp = Keypair::generate();
492 let mut tx = Transaction {
493 version: 1,
494 inputs: vec![TxInput {
495 prev: OutPoint { txid: [1; 32], vout: 0 },
496 pubkey: kp.public_bytes(),
497 signature: vec![],
498 }],
499 outputs: vec![TxOutput { recipient: kp.address(), amount: 42 }],
500 coinbase_data: vec![],
501 shielded: None,
502 contract: None,
503 };
504 let sighash_before = tx.sighash();
505 let txid_before = tx.txid();
506 tx.inputs[0].signature = kp.sign(&sighash_before).to_vec();
507 assert_eq!(tx.sighash(), sighash_before, "sighash must ignore signatures");
508 assert_ne!(tx.txid(), txid_before, "txid commits to signatures");
509
510 tx.outputs[0].amount = 43;
511 assert_ne!(tx.sighash(), sighash_before);
512 }
513
514 #[test]
515 fn coinbase_txids_unique_per_height() {
516 let addr = [9u8; 32];
517 let a = Transaction::coinbase(1, addr, 50, b"blockle");
518 let b = Transaction::coinbase(2, addr, 50, b"blockle");
519 assert!(a.is_coinbase());
520 assert_ne!(a.txid(), b.txid());
521 }
522}