pub use crate::wallet::tx::{Kind, MAX_TX_BYTES, MIN_FEE_PER_BYTE, SIG_BYTES, Tx as Body, VERSION as TX_VERSION};
pub const COINBASE_BYTES: usize = Kind::Coinbase.total_bytes();
pub const TRANSFER_BYTES: usize = Kind::Transfer.total_bytes();
pub const CYBERLINK_BYTES: usize = Kind::Cyberlink.total_bytes();
pub const CLAIM_BYTES: usize = Kind::Claim.total_bytes();
pub const UNLOCK_BYTES: usize = Kind::Unlock.total_bytes();
pub const PAYOUT_BYTES: usize = Kind::Payout.total_bytes();
pub type DecodeError = String;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tx {
pub body: Body,
pub sig: Vec<u8>,
}
pub fn size_of_kind(kind: u8) -> Option<usize> {
Kind::from_byte(kind).ok().map(Kind::total_bytes)
}
pub fn peek_len(bytes: &[u8]) -> Result<usize, DecodeError> {
if bytes.len() < 2 {
return Err("tx shorter than its envelope head".into());
}
if bytes[0] != TX_VERSION {
return Err(format!("tx version {} is not {TX_VERSION}", bytes[0]));
}
size_of_kind(bytes[1]).ok_or_else(|| format!("unknown tx kind {}", bytes[1]))
}
impl Tx {
pub fn kind(&self) -> Kind {
self.body.kind()
}
pub fn is_coinbase(&self) -> bool {
matches!(self.body, Body::Coinbase { .. })
}
pub fn encode_unsigned(&self) -> Vec<u8> {
self.body.encode_unsigned()
}
pub fn encode(&self) -> Vec<u8> {
let mut out = self.encode_unsigned();
out.extend_from_slice(&self.sig);
out
}
pub fn decode(bytes: &[u8]) -> Result<Tx, DecodeError> {
let (body, sig) = Body::decode(bytes).map_err(|e| e.to_string())?;
Ok(Tx { body, sig: sig.map(|s| s.to_vec()).unwrap_or_default() })
}
pub fn from(&self) -> Option<[u8; 32]> {
self.body.from()
}
pub fn nonce(&self) -> Option<u64> {
self.body.nonce()
}
pub fn fee(&self) -> u64 {
self.body.fee().unwrap_or(0)
}
}
pub fn split_body(body: &[u8]) -> Result<Vec<Vec<u8>>, DecodeError> {
if body.len() < 4 {
return Err("body shorter than its count".into());
}
let count = u32::from_le_bytes(body[0..4].try_into().expect("4 bytes")) as usize;
let mut txs = Vec::with_capacity(count.min(1024));
let mut at = 4;
for i in 0..count {
let len = peek_len(&body[at..])?;
if at + len > body.len() {
return Err(format!("tx {i} truncated: needs {len} bytes, {} left", body.len() - at));
}
txs.push(body[at..at + len].to_vec());
at += len;
}
if at != body.len() {
return Err(format!("{} trailing bytes after {count} transactions", body.len() - at));
}
Ok(txs)
}
pub fn join_body(txs: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + txs.iter().map(Vec::len).sum::<usize>());
out.extend_from_slice(&(txs.len() as u32).to_le_bytes());
for t in txs {
out.extend_from_slice(t);
}
out
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
pub(crate) fn coinbase() -> Tx {
Tx { body: Body::Coinbase { height: 1, to: [0x55; 32], amount: 10_000, extra: [0; 32] }, sig: vec![] }
}
#[test]
fn conformance_section_6_coinbase_bytes() {
let bytes = coinbase().encode();
assert_eq!(bytes.len(), COINBASE_BYTES);
assert_eq!(hex(&bytes[..10]), "01000100000000000000");
assert_eq!(hex(&bytes[42..50]), "1027000000000000");
assert_eq!(Tx::decode(&bytes).unwrap(), coinbase());
}
#[test]
fn sizes_and_rejections_every_kind() {
let sig = vec![0u8; SIG_BYTES];
let txs = [
(coinbase(), COINBASE_BYTES),
(Tx { body: Body::Transfer { from: [0x66; 32], nonce: 0, token: [1; 32], amount: 5, to: [0x77; 32], fee: 224 }, sig: sig.clone() }, TRANSFER_BYTES),
(
Tx {
body: Body::Cyberlink { from: [0x66; 32], nonce: 0, token: [1; 32], amount: 0, p: [2; 32], q: [3; 32], valence: -1, fee: 257 },
sig: sig.clone(),
},
CYBERLINK_BYTES,
),
(Tx { body: Body::Claim { account_id: [9; 20], nonce: 3, fee: 140 }, sig: sig.clone() }, CLAIM_BYTES),
(Tx { body: Body::Unlock { from: [0x66; 32], nonce: 4, p: [2; 32], q: [3; 32], amount: 7, fee: 224 }, sig: sig.clone() }, UNLOCK_BYTES),
(Tx { body: Body::Payout { from: [0x66; 32], nonce: 5, p: [2; 32], q: [3; 32], payout: [0x77; 32], fee: 248 }, sig }, PAYOUT_BYTES),
];
assert_eq!([COINBASE_BYTES, TRANSFER_BYTES, CYBERLINK_BYTES, CLAIM_BYTES, UNLOCK_BYTES, PAYOUT_BYTES], [82, 224, 257, 140, 224, 248]);
for (tx, size) in txs {
let bytes = tx.encode();
assert_eq!(bytes.len(), size);
assert_eq!(Tx::decode(&bytes).unwrap(), tx);
let mut trailing = bytes.clone();
trailing.push(0);
assert!(Tx::decode(&trailing).is_err());
assert!(Tx::decode(&bytes[..bytes.len() - 1]).is_err());
let mut v2 = bytes.clone();
v2[0] = 2;
assert!(Tx::decode(&v2).unwrap_err().contains("version 2"));
let mut k4 = bytes.clone();
k4[1] = 4; assert!(Tx::decode(&k4).unwrap_err().contains("kind 4"));
let mut k5 = bytes.clone();
k5[1] = 5;
assert!(Tx::decode(&k5).unwrap_err().contains("kind 5"), "the pool's spend is not a rule-set-1 kind");
for reserved in [8u8, 9, 10] {
let mut k = bytes.clone();
k[1] = reserved;
assert!(Tx::decode(&k).unwrap_err().contains(&format!("kind {reserved}")), "chain-economy §3.5: kinds 8–10 are reserved");
}
}
}
#[test]
fn valence_two_is_rejected() {
let mut bytes = Tx {
body: Body::Cyberlink { from: [0; 32], nonce: 0, token: [0; 32], amount: 0, p: [1; 32], q: [2; 32], valence: 1, fee: 0 },
sig: vec![0; SIG_BYTES],
}
.encode();
bytes[2 + 144] = 2;
assert!(Tx::decode(&bytes).unwrap_err().contains("valence"));
}
#[test]
fn body_split_round_trip() {
let cb = coinbase().encode();
let body = join_body(&[cb.clone(), cb.clone()]);
assert_eq!(split_body(&body).unwrap(), vec![cb.clone(), cb.clone()]);
let mut short = body.clone();
short.pop();
assert!(split_body(&short).is_err());
let mut long = body;
long.push(0);
assert!(split_body(&long).is_err());
assert!(split_body(&[]).is_err());
}
}