1use dusk_bytes::Serializable as DuskSerializable;
8use dusk_core::signatures::bls::PublicKey as AccountPublicKey;
9use dusk_core::transfer::moonlight::Transaction as MoonlightTransaction;
10use dusk_core::transfer::phoenix::Transaction as PhoenixTransaction;
11use dusk_core::transfer::{
12 Transaction as ProtocolTransaction, TransactionFormat,
13};
14use serde::Serialize;
15use sha3::Digest;
16
17use crate::hard_fork;
18
19pub(crate) const LEDGER_TRANSACTION_HEADER_BYTES: usize =
24 std::mem::size_of::<u32>() * 3;
25
26#[derive(Debug, Clone)]
29pub struct CanonicalTransaction {
30 protocol: ProtocolTransaction,
31 format: TransactionFormat,
32}
33
34impl CanonicalTransaction {
35 pub fn canonicalize_for_ledger(
38 protocol: ProtocolTransaction,
39 block_height: u64,
40 ) -> Self {
41 Self::canonicalize(
42 protocol,
43 hard_fork::ledger_tx_format_at(block_height),
44 )
45 }
46
47 pub fn canonicalize_for_ingress(
50 protocol: ProtocolTransaction,
51 block_height: u64,
52 ) -> Self {
53 Self::canonicalize(
54 protocol,
55 hard_fork::ingress_tx_format_at(block_height),
56 )
57 }
58
59 pub fn decode_for_ingress(
66 bytes: &[u8],
67 block_height: u64,
68 ) -> Result<Self, dusk_bytes::Error> {
69 let decoded = Self::decode_any(bytes)?;
70
71 if decoded.format() == TransactionFormat::PreAegis {
72 return Err(dusk_bytes::Error::InvalidData);
73 }
74
75 Ok(decoded.reformat_for_ingress(block_height))
76 }
77
78 pub fn decode_for_ledger(
82 bytes: &[u8],
83 block_height: u64,
84 ) -> Result<Self, dusk_bytes::Error> {
85 Self::decode_with_selected_format(
86 bytes,
87 hard_fork::ledger_tx_format_at(block_height),
88 )
89 }
90
91 pub fn decode_any(bytes: &[u8]) -> Result<Self, dusk_bytes::Error> {
93 let decoded = ProtocolTransaction::decode_any(bytes)?;
94 Ok(Self::from_parts(decoded.transaction, decoded.format))
95 }
96
97 pub fn canonicalize(
98 protocol: ProtocolTransaction,
99 format: TransactionFormat,
100 ) -> Self {
101 Self::from_parts(protocol, format)
102 }
103
104 fn decode_with_selected_format(
105 bytes: &[u8],
106 format: TransactionFormat,
107 ) -> Result<Self, dusk_bytes::Error> {
108 let decoded = ProtocolTransaction::decode_with_format(format, bytes)?;
109 Ok(Self::from_parts(decoded.transaction, decoded.format))
110 }
111
112 fn from_parts(
113 protocol: ProtocolTransaction,
114 format: TransactionFormat,
115 ) -> Self {
116 Self { protocol, format }
117 }
118
119 fn digest_bytes(
120 protocol: &ProtocolTransaction,
121 format: TransactionFormat,
122 ) -> [u8; 32] {
123 let tx_bytes = protocol.blob_to_memo().map_or_else(
124 || protocol.encode_for_format(format),
125 |mut blob_tx| {
126 let _ = blob_tx.strip_blobs();
127 blob_tx.encode_for_format(format)
128 },
129 );
130
131 sha3::Sha3_256::digest(tx_bytes).into()
132 }
133
134 pub fn protocol(&self) -> &ProtocolTransaction {
136 &self.protocol
137 }
138
139 pub fn format(&self) -> TransactionFormat {
141 self.format
142 }
143
144 pub fn reformat_for_ingress(&self, block_height: u64) -> Self {
147 let expected = hard_fork::ingress_tx_format_at(block_height);
148
149 if self.format() == expected {
150 return self.clone();
151 }
152
153 Self::canonicalize(self.protocol.clone(), expected)
154 }
155
156 pub fn protocol_bytes(&self) -> Vec<u8> {
158 self.protocol.encode_for_format(self.format)
159 }
160
161 pub fn id(&self) -> [u8; 32] {
166 self.protocol.hash().to_bytes()
167 }
168
169 pub fn digest(&self) -> [u8; 32] {
174 Self::digest_bytes(&self.protocol, self.format)
175 }
176
177 pub fn gas_price(&self) -> u64 {
178 self.protocol.gas_price()
179 }
180
181 pub fn to_spend_ids(&self) -> Vec<SpendingId> {
182 match &self.protocol {
183 ProtocolTransaction::Phoenix(p) => p
184 .nullifiers()
185 .iter()
186 .map(|n| SpendingId::Nullifier(n.to_bytes()))
187 .collect(),
188 ProtocolTransaction::Moonlight(m) => {
189 vec![SpendingId::AccountNonce(*m.sender(), m.nonce())]
190 }
191 }
192 }
193
194 pub fn next_spending_id(&self) -> Option<SpendingId> {
195 match &self.protocol {
196 ProtocolTransaction::Phoenix(_) => None,
197 ProtocolTransaction::Moonlight(m) => {
198 Some(SpendingId::AccountNonce(*m.sender(), m.nonce() + 1))
199 }
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct LedgerTransaction {
207 pub version: u32,
208 pub r#type: u32,
209 canonical: CanonicalTransaction,
210}
211
212impl LedgerTransaction {
213 pub fn size(&self) -> usize {
215 LEDGER_TRANSACTION_HEADER_BYTES + self.protocol_bytes().len()
216 }
217}
218
219impl From<CanonicalTransaction> for LedgerTransaction {
220 fn from(value: CanonicalTransaction) -> Self {
221 Self {
222 r#type: 1,
223 version: 1,
224 canonical: value,
225 }
226 }
227}
228
229#[derive(Debug, Clone, Serialize)]
232pub struct SpentTransaction {
233 pub inner: LedgerTransaction,
235 pub block_height: u64,
237 pub gas_spent: u64,
240 pub err: Option<String>,
243}
244
245impl SpentTransaction {
246 pub fn public(&self) -> Option<&MoonlightTransaction> {
249 match self.inner.protocol() {
250 ProtocolTransaction::Moonlight(public_tx) => Some(public_tx),
251 _ => None,
252 }
253 }
254
255 pub fn shielded(&self) -> Option<&PhoenixTransaction> {
258 match self.inner.protocol() {
259 ProtocolTransaction::Phoenix(shielded_tx) => Some(shielded_tx),
260 _ => None,
261 }
262 }
263}
264
265impl LedgerTransaction {
266 pub fn decode_for_ingress(
267 bytes: &[u8],
268 block_height: u64,
269 ) -> Result<Self, dusk_bytes::Error> {
270 CanonicalTransaction::decode_for_ingress(bytes, block_height)
271 .map(Into::into)
272 }
273
274 pub fn decode_for_ledger(
275 bytes: &[u8],
276 block_height: u64,
277 ) -> Result<Self, dusk_bytes::Error> {
278 CanonicalTransaction::decode_for_ledger(bytes, block_height)
279 .map(Into::into)
280 }
281
282 pub fn decode_any(bytes: &[u8]) -> Result<Self, dusk_bytes::Error> {
283 CanonicalTransaction::decode_any(bytes).map(Into::into)
284 }
285
286 pub fn from_protocol_with_format(
289 protocol: ProtocolTransaction,
290 format: TransactionFormat,
291 ) -> Self {
292 CanonicalTransaction::canonicalize(protocol, format).into()
293 }
294
295 pub fn from_protocol_for_ledger(
296 protocol: ProtocolTransaction,
297 block_height: u64,
298 ) -> Self {
299 CanonicalTransaction::canonicalize_for_ledger(protocol, block_height)
300 .into()
301 }
302
303 pub fn from_protocol_for_ingress(
304 protocol: ProtocolTransaction,
305 block_height: u64,
306 ) -> Self {
307 CanonicalTransaction::canonicalize_for_ingress(protocol, block_height)
308 .into()
309 }
310
311 pub fn reformat_for_ledger(&self, block_height: u64) -> Self {
314 let expected = hard_fork::ledger_tx_format_at(block_height);
315
316 if self.format() == expected {
317 return self.clone();
318 }
319
320 Self::from_protocol_with_format(self.protocol().clone(), expected)
321 }
322
323 pub fn reformat_for_ingress(&self, block_height: u64) -> Self {
326 let expected = hard_fork::ingress_tx_format_at(block_height);
327
328 if self.format() == expected {
329 return self.clone();
330 }
331
332 Self::from_protocol_with_format(self.protocol().clone(), expected)
333 }
334
335 pub fn format(&self) -> TransactionFormat {
336 self.canonical.format()
337 }
338
339 pub fn canonical(&self) -> &CanonicalTransaction {
340 &self.canonical
341 }
342
343 pub fn protocol(&self) -> &ProtocolTransaction {
344 self.canonical.protocol()
345 }
346
347 pub fn protocol_bytes(&self) -> Vec<u8> {
348 self.canonical.protocol_bytes()
349 }
350
351 pub fn digest(&self) -> [u8; 32] {
362 self.canonical.digest()
363 }
364
365 pub fn id(&self) -> [u8; 32] {
376 self.canonical.id()
377 }
378
379 pub fn gas_price(&self) -> u64 {
380 self.protocol().gas_price()
381 }
382
383 pub fn to_spend_ids(&self) -> Vec<SpendingId> {
384 self.canonical.to_spend_ids()
385 }
386
387 pub fn next_spending_id(&self) -> Option<SpendingId> {
388 self.canonical.next_spending_id()
389 }
390
391 pub fn blob_mut(
392 &mut self,
393 ) -> Option<&mut Vec<dusk_core::transfer::data::BlobData>> {
394 self.canonical.protocol.blob_mut()
395 }
396
397 pub fn strip_blobs(
398 &mut self,
399 ) -> Option<Vec<([u8; 32], dusk_core::transfer::data::BlobSidecar)>> {
400 self.canonical.protocol.strip_blobs()
401 }
402}
403
404impl PartialEq<Self> for LedgerTransaction {
405 fn eq(&self, other: &Self) -> bool {
406 self.r#type == other.r#type
407 && self.version == other.version
408 && self.format() == other.format()
409 && self.id() == other.id()
410 }
411}
412
413impl Eq for LedgerTransaction {}
414
415impl PartialEq<Self> for SpentTransaction {
416 fn eq(&self, other: &Self) -> bool {
417 self.inner == other.inner && self.gas_spent == other.gas_spent
418 }
419}
420
421impl Eq for SpentTransaction {}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
424pub enum SpendingId {
425 Nullifier([u8; 32]),
426 AccountNonce(AccountPublicKey, u64),
427}
428
429impl SpendingId {
430 pub fn to_bytes(&self) -> Vec<u8> {
431 match self {
432 SpendingId::Nullifier(n) => n.to_vec(),
433 SpendingId::AccountNonce(account, nonce) => {
434 let mut id = account.to_bytes().to_vec();
435 id.extend_from_slice(&nonce.to_le_bytes());
436 id
437 }
438 }
439 }
440
441 pub fn next(&self) -> Option<SpendingId> {
442 match self {
443 SpendingId::Nullifier(_) => None,
444 SpendingId::AccountNonce(account, nonce) => {
445 Some(SpendingId::AccountNonce(*account, nonce + 1))
446 }
447 }
448 }
449}
450
451#[cfg(any(feature = "faker", test))]
452pub mod faker {
453 use dusk_core::transfer::data::{ContractCall, TransactionData};
454 use dusk_core::transfer::phoenix::{
455 Fee, Note, Payload as PhoenixPayload, PublicKey as PhoenixPublicKey,
456 SecretKey as PhoenixSecretKey, Transaction as PhoenixTransaction,
457 TxSkeleton,
458 };
459 use dusk_core::{BlsScalar, JubJubScalar};
460 use rand::Rng;
461
462 use super::*;
463 use crate::ledger::Dummy;
464
465 impl<T> Dummy<T> for LedgerTransaction {
466 fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
467 gen_dummy_tx(1_000_000)
468 }
469 }
470
471 impl<T> Dummy<T> for SpentTransaction {
472 fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
473 let tx = LedgerTransaction::from_protocol_with_format(
474 gen_dummy_tx(1_000_000).protocol().clone(),
475 TransactionFormat::PreAegis,
476 );
477 SpentTransaction {
478 inner: tx,
479 block_height: 0,
480 gas_spent: 3,
481 err: Some("error".to_string()),
482 }
483 }
484 }
485
486 pub fn gen_dummy_tx(gas_price: u64) -> LedgerTransaction {
489 let pk = PhoenixPublicKey::from(&PhoenixSecretKey::new(
490 JubJubScalar::from(42u64),
491 JubJubScalar::from(42u64),
492 ));
493 let gas_limit = 1;
494
495 let fee = Fee::deterministic(
496 &JubJubScalar::from(5u64),
497 &pk,
498 gas_limit,
499 gas_price,
500 &[JubJubScalar::from(9u64), JubJubScalar::from(10u64)],
501 );
502
503 let tx_skeleton = TxSkeleton {
504 root: BlsScalar::from(12345u64),
505 nullifiers: vec![
506 BlsScalar::from(1u64),
507 BlsScalar::from(2u64),
508 BlsScalar::from(3u64),
509 ],
510 outputs: [Note::empty(), Note::empty()],
511 max_fee: gas_price * gas_limit,
512 deposit: 0,
513 };
514
515 let contract_call = ContractCall::new([21; 32], "some_method");
516
517 let payload = PhoenixPayload {
518 chain_id: 0xFA,
519 tx_skeleton,
520 fee,
521 data: Some(TransactionData::Call(contract_call)),
522 };
523 let proof = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
524
525 let tx: ProtocolTransaction =
526 PhoenixTransaction::from_payload_and_proof(payload, proof).into();
527
528 LedgerTransaction::from_protocol_with_format(
529 tx,
530 TransactionFormat::Aegis,
531 )
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use dusk_core::transfer::TransactionFormat;
538
539 use super::faker::gen_dummy_tx;
540 use super::*;
541
542 #[test]
543 fn decode_for_ingress_accepts_aegis_and_normalizes_to_boreas() {
544 let tx = gen_dummy_tx(10);
545 let bytes = tx.protocol_bytes();
546
547 let decoded =
548 CanonicalTransaction::decode_for_ingress(&bytes, u64::MAX)
549 .expect("aegis bytes should decode for boreas ingress");
550
551 assert_eq!(decoded.format(), TransactionFormat::Boreas);
552 assert_eq!(decoded.id(), tx.id());
553 }
554
555 #[test]
556 fn decode_for_ingress_accepts_boreas_and_normalizes_to_aegis() {
557 let tx = gen_dummy_tx(10);
558 let bytes = tx.protocol().encode_for_format(TransactionFormat::Boreas);
559
560 let decoded = CanonicalTransaction::decode_for_ingress(&bytes, 1)
561 .expect("boreas bytes should decode for aegis ingress");
562
563 assert_eq!(decoded.format(), TransactionFormat::Aegis);
564 assert_eq!(decoded.id(), tx.id());
565 }
566
567 #[test]
568 fn reformat_for_ingress_preserves_tx_identity() {
569 let tx = gen_dummy_tx(10);
570 let reformatted = tx.reformat_for_ingress(u64::MAX);
571
572 assert_eq!(reformatted.format(), TransactionFormat::Boreas);
573 assert_eq!(reformatted.id(), tx.id());
574 }
575
576 #[test]
577 fn reformat_for_ingress_preserves_tx_identity_before_boreas() {
578 let tx = LedgerTransaction::from_protocol_with_format(
579 gen_dummy_tx(10).protocol().clone(),
580 TransactionFormat::Boreas,
581 );
582 let reformatted = tx.reformat_for_ingress(1);
583
584 assert_eq!(reformatted.format(), TransactionFormat::Aegis);
585 assert_eq!(reformatted.id(), tx.id());
586 }
587}