use serde::{Deserialize, Serialize};
use crate::encode::{put_bytes, put_fixed, put_u32, put_u64};
use crate::hash::{blake2b_256, Hash32};
use crate::keys::Address;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct OutPoint {
pub txid: Hash32,
pub vout: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct TxInput {
pub prev: OutPoint,
pub pubkey: Vec<u8>,
pub signature: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct TxOutput {
pub recipient: Address,
pub amount: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ShieldedSpend {
pub anchor: Hash32,
pub nullifier: Hash32,
pub value: u64,
pub proof: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ShieldedOutput {
pub commitment: Hash32,
pub value: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ShieldedTransfer {
pub anchor: Hash32,
pub nullifier: Hash32,
pub commitment1: Hash32,
pub commitment2: Hash32,
pub fee: u64,
pub memo: Vec<u8>,
pub proof: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ShieldedBundle {
pub spends: Vec<ShieldedSpend>,
pub outputs: Vec<ShieldedOutput>,
#[serde(default)]
pub transfers: Vec<ShieldedTransfer>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ContractAction {
Deploy { code: Vec<u8>, gas_limit: u64 },
Call { contract: Hash32, input: Vec<u8>, value: u64, gas_limit: u64 },
PoolCreate { token: Hash32, block_amt: u64, token_amt: u64, gas_limit: u64 },
PoolAdd { token: Hash32, block_amt: u64, token_max: u64, gas_limit: u64 },
PoolRemove { token: Hash32, shares: u64, gas_limit: u64 },
PoolSwapBuy { token: Hash32, block_in: u64, min_token_out: u64, gas_limit: u64 },
PoolSwapSell { token: Hash32, token_in: u64, min_block_out: u64, gas_limit: u64 },
LaunchToken { code: Vec<u8>, block_amt: u64, token_amt: u64, gas_limit: u64 },
LaunchNft { code: Vec<u8>, gas_limit: u64 },
}
impl ContractAction {
pub fn gas_limit(&self) -> u64 {
match self {
ContractAction::Deploy { gas_limit, .. } => *gas_limit,
ContractAction::Call { gas_limit, .. } => *gas_limit,
ContractAction::PoolCreate { gas_limit, .. } => *gas_limit,
ContractAction::PoolAdd { gas_limit, .. } => *gas_limit,
ContractAction::PoolRemove { gas_limit, .. } => *gas_limit,
ContractAction::PoolSwapBuy { gas_limit, .. } => *gas_limit,
ContractAction::PoolSwapSell { gas_limit, .. } => *gas_limit,
ContractAction::LaunchToken { gas_limit, .. } => *gas_limit,
ContractAction::LaunchNft { gas_limit, .. } => *gas_limit,
}
}
pub fn value(&self) -> u64 {
match self {
ContractAction::Deploy { .. } => 0,
ContractAction::Call { value, .. } => *value,
ContractAction::PoolCreate { block_amt, .. } => *block_amt,
ContractAction::PoolAdd { block_amt, .. } => *block_amt,
ContractAction::PoolRemove { .. } => 0,
ContractAction::PoolSwapBuy { block_in, .. } => *block_in,
ContractAction::PoolSwapSell { .. } => 0,
ContractAction::LaunchToken { block_amt, .. } => *block_amt,
ContractAction::LaunchNft { .. } => 0,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Transaction {
pub version: u32,
pub inputs: Vec<TxInput>,
pub outputs: Vec<TxOutput>,
pub coinbase_data: Vec<u8>,
pub shielded: Option<ShieldedBundle>,
#[serde(default)]
pub contract: Option<ContractAction>,
}
impl Transaction {
pub fn is_coinbase(&self) -> bool {
self.inputs.is_empty() && self.shielded.is_none()
}
pub fn shielded_in(&self) -> Option<u64> {
self.shielded.as_ref().map_or(Some(0), |b| {
b.spends.iter().try_fold(0u64, |acc, s| acc.checked_add(s.value))
})
}
pub fn shielded_out(&self) -> Option<u64> {
self.shielded.as_ref().map_or(Some(0), |b| {
b.outputs.iter().try_fold(0u64, |acc, o| acc.checked_add(o.value))
})
}
pub fn transfer_fees(&self) -> Option<u64> {
self.shielded.as_ref().map_or(Some(0), |b| {
b.transfers.iter().try_fold(0u64, |acc, t| acc.checked_add(t.fee))
})
}
pub fn nullifiers(&self) -> Vec<Hash32> {
self.shielded
.as_ref()
.map(|b| {
b.spends
.iter()
.map(|s| s.nullifier)
.chain(b.transfers.iter().map(|t| t.nullifier))
.collect()
})
.unwrap_or_default()
}
pub fn encode(&self, for_sighash: bool) -> Vec<u8> {
let mut out = Vec::new();
put_u32(&mut out, self.version);
put_u32(&mut out, self.inputs.len() as u32);
for input in &self.inputs {
put_fixed(&mut out, &input.prev.txid);
put_u32(&mut out, input.prev.vout);
put_bytes(&mut out, &input.pubkey);
if for_sighash {
put_bytes(&mut out, &[]);
} else {
put_bytes(&mut out, &input.signature);
}
}
put_u32(&mut out, self.outputs.len() as u32);
for output in &self.outputs {
put_fixed(&mut out, &output.recipient);
put_u64(&mut out, output.amount);
}
put_bytes(&mut out, &self.coinbase_data);
match &self.shielded {
None => put_u32(&mut out, 0),
Some(s) => {
put_u32(&mut out, 1);
put_u32(&mut out, s.spends.len() as u32);
for spend in &s.spends {
put_fixed(&mut out, &spend.anchor);
put_fixed(&mut out, &spend.nullifier);
put_u64(&mut out, spend.value);
if for_sighash {
put_bytes(&mut out, &[]);
} else {
put_bytes(&mut out, &spend.proof);
}
}
put_u32(&mut out, s.outputs.len() as u32);
for output in &s.outputs {
put_fixed(&mut out, &output.commitment);
put_u64(&mut out, output.value);
}
put_u32(&mut out, s.transfers.len() as u32);
for t in &s.transfers {
put_fixed(&mut out, &t.anchor);
put_fixed(&mut out, &t.nullifier);
put_fixed(&mut out, &t.commitment1);
put_fixed(&mut out, &t.commitment2);
put_u64(&mut out, t.fee);
put_bytes(&mut out, &t.memo);
if for_sighash {
put_bytes(&mut out, &[]);
} else {
put_bytes(&mut out, &t.proof);
}
}
}
}
match &self.contract {
None => put_u32(&mut out, 0),
Some(ContractAction::Deploy { code, gas_limit }) => {
put_u32(&mut out, 1);
put_bytes(&mut out, code);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::Call { contract, input, value, gas_limit }) => {
put_u32(&mut out, 2);
put_fixed(&mut out, contract);
put_bytes(&mut out, input);
put_u64(&mut out, *value);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::PoolCreate { token, block_amt, token_amt, gas_limit }) => {
put_u32(&mut out, 3);
put_fixed(&mut out, token);
put_u64(&mut out, *block_amt);
put_u64(&mut out, *token_amt);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::PoolAdd { token, block_amt, token_max, gas_limit }) => {
put_u32(&mut out, 4);
put_fixed(&mut out, token);
put_u64(&mut out, *block_amt);
put_u64(&mut out, *token_max);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::PoolRemove { token, shares, gas_limit }) => {
put_u32(&mut out, 5);
put_fixed(&mut out, token);
put_u64(&mut out, *shares);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::PoolSwapBuy { token, block_in, min_token_out, gas_limit }) => {
put_u32(&mut out, 6);
put_fixed(&mut out, token);
put_u64(&mut out, *block_in);
put_u64(&mut out, *min_token_out);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::PoolSwapSell { token, token_in, min_block_out, gas_limit }) => {
put_u32(&mut out, 7);
put_fixed(&mut out, token);
put_u64(&mut out, *token_in);
put_u64(&mut out, *min_block_out);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::LaunchToken { code, block_amt, token_amt, gas_limit }) => {
put_u32(&mut out, 8);
put_bytes(&mut out, code);
put_u64(&mut out, *block_amt);
put_u64(&mut out, *token_amt);
put_u64(&mut out, *gas_limit);
}
Some(ContractAction::LaunchNft { code, gas_limit }) => {
put_u32(&mut out, 9);
put_bytes(&mut out, code);
put_u64(&mut out, *gas_limit);
}
}
out
}
pub fn txid(&self) -> Hash32 {
blake2b_256(&self.encode(false))
}
pub fn sighash(&self) -> Hash32 {
blake2b_256(&self.encode(true))
}
pub fn total_output(&self) -> Option<u64> {
self.outputs
.iter()
.try_fold(0u64, |acc, o| acc.checked_add(o.amount))
}
pub fn serialized_size(&self) -> usize {
self.encode(false).len()
}
pub fn coinbase(height: u64, recipient: Address, amount: u64, tag: &[u8]) -> Self {
let mut coinbase_data = height.to_le_bytes().to_vec();
coinbase_data.extend_from_slice(tag);
Transaction {
version: 1,
inputs: vec![],
outputs: vec![TxOutput { recipient, amount }],
coinbase_data,
shielded: None,
contract: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::Keypair;
#[test]
fn txid_changes_with_content_sighash_ignores_sigs() {
let kp = Keypair::generate();
let mut tx = Transaction {
version: 1,
inputs: vec![TxInput {
prev: OutPoint { txid: [1; 32], vout: 0 },
pubkey: kp.public_bytes(),
signature: vec![],
}],
outputs: vec![TxOutput { recipient: kp.address(), amount: 42 }],
coinbase_data: vec![],
shielded: None,
contract: None,
};
let sighash_before = tx.sighash();
let txid_before = tx.txid();
tx.inputs[0].signature = kp.sign(&sighash_before).to_vec();
assert_eq!(tx.sighash(), sighash_before, "sighash must ignore signatures");
assert_ne!(tx.txid(), txid_before, "txid commits to signatures");
tx.outputs[0].amount = 43;
assert_ne!(tx.sighash(), sighash_before);
}
#[test]
fn coinbase_txids_unique_per_height() {
let addr = [9u8; 32];
let a = Transaction::coinbase(1, addr, 50, b"blockle");
let b = Transaction::coinbase(2, addr, 50, b"blockle");
assert!(a.is_coinbase());
assert_ne!(a.txid(), b.txid());
}
}