use super::Error;
pub const VERSION: u8 = 1;
pub const MIN_FEE_PER_BYTE: u64 = 1;
pub const MAX_TX_BYTES: usize = 512;
pub const SIG_BYTES: usize = 102;
const SIGN_TAG: &[u8] = b"soft3/chain/sign/v1\0";
const TXID_TAG: &[u8] = b"soft3/chain/txid/v1\0";
const LEAF_TAG: &[u8] = b"soft3/chain/leaf/v1\0";
const NODE_TAG: &[u8] = b"soft3/chain/node/v1\0";
const LEGACY_TAG: &[u8] = b"soft3/chain/legacy/v1\0";
const TOPIC_TAG: &[u8] = b"soft3/chain/topic/v1\0";
const CARD_TAG: &[u8] = b"soft3/chain/card/v1\0";
const STAKE_TAG: &[u8] = b"soft3/chain/stake/v1\0";
const FOCUS_TAG: &[u8] = b"soft3/chain/focus/v1\0";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Kind {
Coinbase = 0,
Transfer = 1,
Cyberlink = 2,
Claim = 3,
Unlock = 6,
Payout = 7,
}
impl Kind {
pub fn from_byte(b: u8) -> Result<Self, Error> {
match b {
0 => Ok(Self::Coinbase),
1 => Ok(Self::Transfer),
2 => Ok(Self::Cyberlink),
3 => Ok(Self::Claim),
6 => Ok(Self::Unlock),
7 => Ok(Self::Payout),
other => Err(Error::Encoding(format!("unknown tx kind {other}"))),
}
}
pub const fn body_bytes(self) -> usize {
match self {
Self::Coinbase => 80,
Self::Transfer => 120,
Self::Cyberlink => 153,
Self::Claim => 36,
Self::Unlock => 120,
Self::Payout => 144,
}
}
pub const fn signed(self) -> bool {
!matches!(self, Self::Coinbase)
}
pub const fn total_bytes(self) -> usize {
2 + self.body_bytes() + if self.signed() { SIG_BYTES } else { 0 }
}
pub const fn min_fee(self) -> u64 {
self.total_bytes() as u64 * MIN_FEE_PER_BYTE
}
pub const fn name(self) -> &'static str {
match self {
Self::Coinbase => "coinbase",
Self::Transfer => "transfer",
Self::Cyberlink => "cyberlink",
Self::Claim => "claim",
Self::Unlock => "unlock",
Self::Payout => "payout",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Tx {
Coinbase { height: u64, to: [u8; 32], amount: u64, extra: [u8; 32] },
Transfer { from: [u8; 32], nonce: u64, token: [u8; 32], amount: u64, to: [u8; 32], fee: u64 },
Cyberlink {
from: [u8; 32],
nonce: u64,
token: [u8; 32],
amount: u64,
p: [u8; 32],
q: [u8; 32],
valence: i8,
fee: u64,
},
Claim { account_id: [u8; 20], nonce: u64, fee: u64 },
Unlock { from: [u8; 32], nonce: u64, p: [u8; 32], q: [u8; 32], amount: u64, fee: u64 },
Payout { from: [u8; 32], nonce: u64, p: [u8; 32], q: [u8; 32], payout: [u8; 32], fee: u64 },
}
impl Tx {
pub fn kind(&self) -> Kind {
match self {
Tx::Coinbase { .. } => Kind::Coinbase,
Tx::Transfer { .. } => Kind::Transfer,
Tx::Cyberlink { .. } => Kind::Cyberlink,
Tx::Claim { .. } => Kind::Claim,
Tx::Unlock { .. } => Kind::Unlock,
Tx::Payout { .. } => Kind::Payout,
}
}
pub fn nonce(&self) -> Option<u64> {
match self {
Tx::Coinbase { .. } => None,
Tx::Transfer { nonce, .. }
| Tx::Cyberlink { nonce, .. }
| Tx::Claim { nonce, .. }
| Tx::Unlock { nonce, .. }
| Tx::Payout { nonce, .. } => Some(*nonce),
}
}
pub fn fee(&self) -> Option<u64> {
match self {
Tx::Coinbase { .. } => None,
Tx::Transfer { fee, .. }
| Tx::Cyberlink { fee, .. }
| Tx::Claim { fee, .. }
| Tx::Unlock { fee, .. }
| Tx::Payout { fee, .. } => Some(*fee),
}
}
pub fn from(&self) -> Option<[u8; 32]> {
match self {
Tx::Transfer { from, .. } | Tx::Cyberlink { from, .. } | Tx::Unlock { from, .. } | Tx::Payout { from, .. } => {
Some(*from)
}
Tx::Coinbase { .. } | Tx::Claim { .. } => None,
}
}
pub fn check_rules(&self) -> Result<(), Error> {
let kind = self.kind();
if let Some(fee) = self.fee() {
if fee < kind.min_fee() {
return Err(Error::Rule(format!(
"fee {fee} below the floor {} for a {}",
kind.min_fee(),
kind.name()
)));
}
}
if let Tx::Cyberlink { p, q, valence, .. } = self {
if p == q {
return Err(Error::Rule("cyberlink requires p != q".into()));
}
if !(-1..=1).contains(valence) {
return Err(Error::Rule(format!("valence {valence} outside -1..=1")));
}
}
if let Tx::Unlock { amount: 0, .. } = self {
return Err(Error::Rule("unlock of 0 does nothing".into()));
}
Ok(())
}
pub fn encode_unsigned(&self) -> Vec<u8> {
let kind = self.kind();
let mut out = Vec::with_capacity(kind.total_bytes());
out.push(VERSION);
out.push(kind as u8);
match self {
Tx::Coinbase { height, to, amount, extra } => {
out.extend_from_slice(&height.to_le_bytes());
out.extend_from_slice(to);
out.extend_from_slice(&amount.to_le_bytes());
out.extend_from_slice(extra);
}
Tx::Transfer { from, nonce, token, amount, to, fee } => {
out.extend_from_slice(from);
out.extend_from_slice(&nonce.to_le_bytes());
out.extend_from_slice(token);
out.extend_from_slice(&amount.to_le_bytes());
out.extend_from_slice(to);
out.extend_from_slice(&fee.to_le_bytes());
}
Tx::Cyberlink { from, nonce, token, amount, p, q, valence, fee } => {
out.extend_from_slice(from);
out.extend_from_slice(&nonce.to_le_bytes());
out.extend_from_slice(token);
out.extend_from_slice(&amount.to_le_bytes());
out.extend_from_slice(p);
out.extend_from_slice(q);
out.push(*valence as u8);
out.extend_from_slice(&fee.to_le_bytes());
}
Tx::Claim { account_id, nonce, fee } => {
out.extend_from_slice(account_id);
out.extend_from_slice(&nonce.to_le_bytes());
out.extend_from_slice(&fee.to_le_bytes());
}
Tx::Unlock { from, nonce, p, q, amount, fee } => {
out.extend_from_slice(from);
out.extend_from_slice(&nonce.to_le_bytes());
out.extend_from_slice(p);
out.extend_from_slice(q);
out.extend_from_slice(&amount.to_le_bytes());
out.extend_from_slice(&fee.to_le_bytes());
}
Tx::Payout { from, nonce, p, q, payout, fee } => {
out.extend_from_slice(from);
out.extend_from_slice(&nonce.to_le_bytes());
out.extend_from_slice(p);
out.extend_from_slice(q);
out.extend_from_slice(payout);
out.extend_from_slice(&fee.to_le_bytes());
}
}
debug_assert_eq!(out.len(), 2 + kind.body_bytes());
out
}
pub fn decode(bytes: &[u8]) -> Result<(Tx, Option<[u8; SIG_BYTES]>), Error> {
if bytes.len() < 2 {
return Err(Error::Encoding("tx shorter than its envelope head".into()));
}
if bytes[0] != VERSION {
return Err(Error::Encoding(format!("tx version {} is not {VERSION}", bytes[0])));
}
let kind = Kind::from_byte(bytes[1])?;
if bytes.len() != kind.total_bytes() {
return Err(Error::Encoding(format!(
"{} tx is {} bytes, got {}",
kind.name(),
kind.total_bytes(),
bytes.len()
)));
}
let mut r = Reader { bytes, at: 2 };
let tx = match kind {
Kind::Coinbase => Tx::Coinbase {
height: r.u64(),
to: r.arr32(),
amount: r.u64(),
extra: r.arr32(),
},
Kind::Transfer => Tx::Transfer {
from: r.arr32(),
nonce: r.u64(),
token: r.arr32(),
amount: r.u64(),
to: r.arr32(),
fee: r.u64(),
},
Kind::Cyberlink => {
let (from, nonce, token, amount, p, q) =
(r.arr32(), r.u64(), r.arr32(), r.u64(), r.arr32(), r.arr32());
let valence = r.u8() as i8;
if !(-1..=1).contains(&valence) {
return Err(Error::Encoding(format!("valence byte {valence} is not -1, 0 or 1")));
}
Tx::Cyberlink { from, nonce, token, amount, p, q, valence, fee: r.u64() }
}
Kind::Claim => Tx::Claim { account_id: r.arr20(), nonce: r.u64(), fee: r.u64() },
Kind::Unlock => Tx::Unlock { from: r.arr32(), nonce: r.u64(), p: r.arr32(), q: r.arr32(), amount: r.u64(), fee: r.u64() },
Kind::Payout => Tx::Payout { from: r.arr32(), nonce: r.u64(), p: r.arr32(), q: r.arr32(), payout: r.arr32(), fee: r.u64() },
};
let sig = if kind.signed() {
let mut sig = [0u8; SIG_BYTES];
sig.copy_from_slice(&bytes[r.at..r.at + SIG_BYTES]);
Some(sig)
} else {
None
};
Ok((tx, sig))
}
}
struct Reader<'a> {
bytes: &'a [u8],
at: usize,
}
impl Reader<'_> {
fn u8(&mut self) -> u8 {
let v = self.bytes[self.at];
self.at += 1;
v
}
fn u64(&mut self) -> u64 {
let mut b = [0u8; 8];
b.copy_from_slice(&self.bytes[self.at..self.at + 8]);
self.at += 8;
u64::from_le_bytes(b)
}
fn arr32(&mut self) -> [u8; 32] {
let mut b = [0u8; 32];
b.copy_from_slice(&self.bytes[self.at..self.at + 32]);
self.at += 32;
b
}
fn arr20(&mut self) -> [u8; 20] {
let mut b = [0u8; 20];
b.copy_from_slice(&self.bytes[self.at..self.at + 20]);
self.at += 20;
b
}
}
fn h(parts: &[&[u8]]) -> [u8; 32] {
let mut input = Vec::new();
for p in parts {
input.extend_from_slice(p);
}
*hemera::hash(&input).as_bytes()
}
pub fn statement(chain: &[u8; 32], unsigned: &[u8]) -> [u8; 32] {
h(&[SIGN_TAG, chain, unsigned])
}
pub fn txid(chain: &[u8; 32], tx_bytes: &[u8]) -> [u8; 32] {
h(&[TXID_TAG, chain, tx_bytes])
}
pub fn leaf(tx_bytes: &[u8]) -> [u8; 32] {
h(&[LEAF_TAG, tx_bytes])
}
pub fn node(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
h(&[NODE_TAG, left, right])
}
pub fn tx_root(txs: &[Vec<u8>]) -> Option<[u8; 32]> {
let mut level: Vec<[u8; 32]> = txs.iter().map(|t| leaf(t)).collect();
if level.is_empty() {
return None;
}
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len() / 2 + 1);
for pair in level.chunks(2) {
match pair {
[l, r] => next.push(node(l, r)),
[last] => next.push(*last),
_ => unreachable!(),
}
}
level = next;
}
Some(level[0])
}
pub fn token(denom: &str) -> [u8; 32] {
*hemera::hash(denom.as_bytes()).as_bytes()
}
pub fn neuron_of(pubkey: &[u8; 33]) -> [u8; 32] {
mudra::claim::neuron_of(pubkey)
}
pub fn legacy_owner(hrp: &str, account_id: &[u8; 20]) -> [u8; 32] {
h(&[LEGACY_TAG, hrp.as_bytes(), account_id])
}
pub fn topic(chain: &[u8; 32], name: &str) -> [u8; 32] {
h(&[TOPIC_TAG, chain, name.as_bytes()])
}
pub fn axon(p: &[u8; 32], q: &[u8; 32]) -> [u8; 32] {
h(&[p, q])
}
pub fn card_id(author: &[u8; 32], p: &[u8; 32], q: &[u8; 32]) -> [u8; 32] {
h(&[CARD_TAG, author, p, q])
}
pub fn stake_key(card: &[u8; 32]) -> [u8; 32] {
h(&[STAKE_TAG, card])
}
pub fn focus_key(token: &[u8; 32]) -> [u8; 32] {
h(&[FOCUS_TAG, token])
}
pub fn balance_key(owner: &[u8; 32], token: &[u8; 32]) -> [u8; 32] {
h(&[owner, token])
}