iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

transaction.rs (17561B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use anyhow::{Context, Result, bail};
      4 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
      5 use serde::{Deserialize, Serialize};
      6 
      7 use super::{
      8     Amount, MINE_FINALIZER_FEE, MINE_REWARD, PUBLIC_KEY_BYTES, SIGNATURE_BYTES, Wallet,
      9     canonical_transaction_size_bytes, decode_hex_array, genesis_allocation_outpoint,
     10     hash_meets_difficulty, hex_encode, hex_hash, mine_payload, mine_signature,
     11     stratum_mine_header_bytes, stratum_mine_signature,
     12 };
     13 
     14 #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
     15 pub struct OutPoint {
     16     pub txid: String,
     17     pub index: u32,
     18 }
     19 
     20 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     21 pub struct TxInput {
     22     pub outpoint: OutPoint,
     23     pub owner: String,
     24     pub signature: String,
     25 }
     26 
     27 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     28 pub struct TxOutput {
     29     pub address: String,
     30     pub amount: Amount,
     31 }
     32 
     33 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     34 #[serde(tag = "kind", rename_all = "snake_case")]
     35 pub enum Transaction {
     36     Transfer {
     37         inputs: Vec<TxInput>,
     38         outputs: Vec<TxOutput>,
     39         #[serde(default)]
     40         fee: Amount,
     41         signature: String,
     42     },
     43     Burn {
     44         inputs: Vec<TxInput>,
     45         change: Vec<TxOutput>,
     46         amount: Amount,
     47         #[serde(default)]
     48         fee: Amount,
     49         signature: String,
     50     },
     51     Mine {
     52         recipient: String,
     53         anchor: String,
     54         #[serde(default)]
     55         salt: u64,
     56         nonce: u64,
     57         difficulty_bits: u32,
     58         #[serde(default, skip_serializing_if = "Option::is_none")]
     59         proof_header: Option<String>,
     60         signature: String,
     61     },
     62 }
     63 
     64 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     65 #[serde(tag = "kind", rename_all = "snake_case")]
     66 pub(super) enum BlindedTransactionPayload {
     67     Transfer {
     68         outputs: Vec<TxOutput>,
     69         signature: String,
     70     },
     71     Burn {
     72         change: Vec<TxOutput>,
     73         amount: Amount,
     74         signature: String,
     75     },
     76 }
     77 
     78 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     79 #[serde(rename_all = "camelCase")]
     80 pub struct BlindedTransaction {
     81     pub commitment: String,
     82     #[serde(default)]
     83     pub inputs: Vec<TxInput>,
     84     pub fee: Amount,
     85     pub encrypted_size: u32,
     86     pub expires_at_height: u64,
     87     pub nonce: String,
     88     pub ciphertext: String,
     89     pub payload_hash: String,
     90 }
     91 
     92 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     93 #[serde(rename_all = "camelCase")]
     94 pub struct BlindedReveal {
     95     pub commitment: String,
     96     pub key: String,
     97 }
     98 
     99 #[derive(Clone, Debug, Eq, PartialEq)]
    100 pub struct BuiltBlindedTransaction {
    101     pub payload: Transaction,
    102     pub transaction: BlindedTransaction,
    103     pub reveal: BlindedReveal,
    104 }
    105 
    106 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    107 #[serde(rename_all = "camelCase")]
    108 pub struct OwnedBlindedTransaction {
    109     pub transaction: BlindedTransaction,
    110     pub payload: Transaction,
    111     pub reveal: BlindedReveal,
    112 }
    113 
    114 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    115 #[serde(rename_all = "camelCase")]
    116 pub struct RevealedBlindedTransaction {
    117     pub height: u64,
    118     pub commitment: String,
    119     pub included_by: String,
    120     pub transaction: Transaction,
    121 }
    122 
    123 #[derive(Clone, Debug, Eq, PartialEq)]
    124 pub struct MineSearchOutcome {
    125     pub transaction: Option<Transaction>,
    126     pub next_nonce: u64,
    127     pub attempts: u64,
    128 }
    129 
    130 impl Transaction {
    131     pub fn genesis_burn(from: impl Into<String>, amount: Amount) -> Self {
    132         let from = from.into();
    133         Self::genesis_burn_with_change(from, amount, Vec::new())
    134     }
    135 
    136     pub(super) fn genesis_burn_with_allocation(
    137         from: impl Into<String>,
    138         amount: Amount,
    139         allocation: Amount,
    140     ) -> Result<Self> {
    141         if amount > allocation {
    142             bail!("genesis burn exceeds allocation");
    143         }
    144         let from = from.into();
    145         let change_amount = allocation - amount;
    146         let change = if change_amount > 0 {
    147             vec![TxOutput {
    148                 address: from.clone(),
    149                 amount: change_amount,
    150             }]
    151         } else {
    152             Vec::new()
    153         };
    154         Ok(Self::genesis_burn_with_change(from, amount, change))
    155     }
    156 
    157     fn genesis_burn_with_change(from: String, amount: Amount, change: Vec<TxOutput>) -> Self {
    158         let input = TxInput {
    159             outpoint: genesis_allocation_outpoint(&from),
    160             owner: from.clone(),
    161             signature: "genesis".to_string(),
    162         };
    163         let unsigned = UnsignedUtxoTransaction::Burn {
    164             inputs: vec![input.without_signature()],
    165             change: change.clone(),
    166             amount,
    167             fee: 0,
    168         };
    169         let signature = hex_hash(format!("iuna-genesis-burn:{}", unsigned.canonical()));
    170         Self::Burn {
    171             inputs: vec![input],
    172             change,
    173             amount,
    174             fee: 0,
    175             signature,
    176         }
    177     }
    178 
    179     pub fn sender(&self) -> &str {
    180         match self {
    181             Self::Transfer { inputs, .. } | Self::Burn { inputs, .. } => inputs
    182                 .first()
    183                 .map(|input| input.owner.as_str())
    184                 .unwrap_or(""),
    185             Self::Mine { recipient, .. } => recipient.as_str(),
    186         }
    187     }
    188 
    189     pub fn to(&self) -> Option<&str> {
    190         match self {
    191             Self::Transfer { outputs, .. } => outputs.first().map(|output| output.address.as_str()),
    192             Self::Burn { .. } => None,
    193             Self::Mine { recipient, .. } => Some(recipient.as_str()),
    194         }
    195     }
    196 
    197     pub fn amount(&self) -> Amount {
    198         match self {
    199             Self::Transfer { outputs, .. } => {
    200                 outputs.first().map(|output| output.amount).unwrap_or(0)
    201             }
    202             Self::Burn { amount, .. } => *amount,
    203             Self::Mine { .. } => MINE_REWARD,
    204         }
    205     }
    206 
    207     pub fn fee(&self) -> Amount {
    208         match self {
    209             Self::Transfer { fee, .. } | Self::Burn { fee, .. } => *fee,
    210             Self::Mine { .. } => MINE_FINALIZER_FEE,
    211         }
    212     }
    213 
    214     pub fn total_debit(&self) -> Result<Amount> {
    215         if matches!(self, Self::Mine { .. }) {
    216             return Ok(0);
    217         }
    218         self.amount()
    219             .checked_add(self.fee())
    220             .context("transaction amount plus fee overflows")
    221     }
    222 
    223     pub fn signature(&self) -> &str {
    224         match self {
    225             Self::Transfer { signature, .. } | Self::Burn { signature, .. } => signature,
    226             Self::Mine { signature, .. } => signature,
    227         }
    228     }
    229 
    230     pub fn is_burn(&self) -> bool {
    231         matches!(self, Self::Burn { .. })
    232     }
    233 
    234     pub fn canonical(&self) -> String {
    235         format!("{}:{}", self.signing_payload(), self.signature())
    236     }
    237 
    238     pub fn economic_size_bytes(&self) -> usize {
    239         canonical_transaction_size_bytes(self)
    240     }
    241 
    242     pub fn serialized_size_bytes(&self) -> Result<usize> {
    243         serde_json::to_vec(self)
    244             .map(|bytes| bytes.len())
    245             .context("failed to serialize transaction for size check")
    246     }
    247 
    248     fn signing_payload(&self) -> String {
    249         match self {
    250             Self::Transfer {
    251                 inputs,
    252                 outputs,
    253                 fee,
    254                 ..
    255             } => UnsignedUtxoTransaction::Transfer {
    256                 inputs: unsigned_inputs(inputs),
    257                 outputs: outputs.clone(),
    258                 fee: *fee,
    259             }
    260             .canonical(),
    261             Self::Burn {
    262                 inputs,
    263                 change,
    264                 amount,
    265                 fee,
    266                 ..
    267             } => UnsignedUtxoTransaction::Burn {
    268                 inputs: unsigned_inputs(inputs),
    269                 change: change.clone(),
    270                 amount: *amount,
    271                 fee: *fee,
    272             }
    273             .canonical(),
    274             Self::Mine {
    275                 recipient,
    276                 anchor,
    277                 salt,
    278                 nonce,
    279                 difficulty_bits,
    280                 ..
    281             } => mine_payload(recipient, anchor, *salt, *nonce, *difficulty_bits),
    282         }
    283     }
    284 
    285     pub(super) fn verify_signature(&self) -> Result<()> {
    286         if let Self::Mine {
    287             recipient,
    288             anchor,
    289             salt,
    290             nonce,
    291             difficulty_bits,
    292             proof_header,
    293             signature,
    294         } = self
    295         {
    296             let expected = if let Some(proof_header) = proof_header {
    297                 let header =
    298                     stratum_mine_header_bytes(recipient, anchor, *salt, *nonce, *difficulty_bits)?;
    299                 let expected_header = hex_encode(header);
    300                 if *proof_header != expected_header {
    301                     bail!("mine transaction proof header is invalid");
    302                 }
    303                 stratum_mine_signature(&header)
    304             } else {
    305                 mine_signature(recipient, anchor, *salt, *nonce, *difficulty_bits)
    306             };
    307             if *signature != expected {
    308                 bail!("mine transaction proof hash is invalid");
    309             }
    310             if !hash_meets_difficulty(signature, *difficulty_bits) {
    311                 bail!("mine transaction proof does not meet difficulty");
    312             }
    313             return Ok(());
    314         }
    315         if self.signature().starts_with("iuna-genesis-burn:") || self.inputs_are_genesis_signed() {
    316             return Ok(());
    317         }
    318         if !self
    319             .inputs()
    320             .iter()
    321             .all(|input| input.signature == self.signature())
    322         {
    323             bail!("transaction input signature does not match transaction signature");
    324         }
    325         let sender = self.sender();
    326         let public_key = decode_hex_array::<PUBLIC_KEY_BYTES>(sender)
    327             .with_context(|| format!("invalid public key for {sender}"))?;
    328         let signature = decode_hex_array::<SIGNATURE_BYTES>(self.signature())
    329             .context("invalid signature hex")?;
    330         let verifying_key =
    331             VerifyingKey::from_bytes(&public_key).context("invalid transaction public key")?;
    332         let signature = Signature::from_bytes(&signature);
    333         verifying_key
    334             .verify(self.signing_payload().as_bytes(), &signature)
    335             .context("transaction signature is invalid")
    336     }
    337 
    338     pub(super) fn inputs(&self) -> &[TxInput] {
    339         match self {
    340             Self::Transfer { inputs, .. } | Self::Burn { inputs, .. } => inputs,
    341             Self::Mine { .. } => &[],
    342         }
    343     }
    344 
    345     pub(super) fn outputs(&self) -> Vec<TxOutput> {
    346         match self {
    347             Self::Transfer { outputs, .. } => outputs.clone(),
    348             Self::Burn { change, .. } => change.clone(),
    349             Self::Mine { recipient, .. } => vec![TxOutput {
    350                 address: recipient.clone(),
    351                 amount: MINE_REWARD,
    352             }],
    353         }
    354     }
    355 
    356     fn inputs_are_genesis_signed(&self) -> bool {
    357         self.inputs()
    358             .iter()
    359             .all(|input| input.signature == "genesis")
    360     }
    361 }
    362 
    363 impl BlindedTransaction {
    364     pub fn id(&self) -> &str {
    365         &self.commitment
    366     }
    367 
    368     pub fn canonical(&self) -> String {
    369         format!(
    370             "blinded-tx:{}:{}:{}:{}:{}:{}:{}",
    371             canonical_signed_inputs(&self.inputs),
    372             self.fee,
    373             self.encrypted_size,
    374             self.expires_at_height,
    375             self.nonce,
    376             self.ciphertext,
    377             self.payload_hash
    378         )
    379     }
    380 
    381     pub fn fee_rate_size_bytes(&self) -> usize {
    382         self.serialized_size_bytes()
    383             .unwrap_or(self.encrypted_size as usize)
    384     }
    385 
    386     pub fn serialized_size_bytes(&self) -> Result<usize> {
    387         serde_json::to_vec(self)
    388             .map(|bytes| bytes.len())
    389             .context("failed to serialize blinded transaction for size check")
    390     }
    391 }
    392 
    393 impl BlindedReveal {
    394     pub fn canonical(&self) -> String {
    395         format!("blinded-reveal:{}:{}", self.commitment, self.key)
    396     }
    397 }
    398 
    399 impl TxInput {
    400     pub(super) fn without_signature(&self) -> UnsignedTxInput {
    401         UnsignedTxInput {
    402             outpoint: self.outpoint.clone(),
    403             owner: self.owner.clone(),
    404         }
    405     }
    406 }
    407 
    408 impl OutPoint {
    409     pub(super) fn id(&self) -> String {
    410         format!("{}:{}", self.txid, self.index)
    411     }
    412 }
    413 
    414 #[derive(Clone, Debug, Eq, PartialEq)]
    415 pub(super) struct UnsignedTxInput {
    416     pub(super) outpoint: OutPoint,
    417     pub(super) owner: String,
    418 }
    419 
    420 #[derive(Clone, Debug, Eq, PartialEq)]
    421 pub(super) enum UnsignedUtxoTransaction {
    422     Transfer {
    423         inputs: Vec<UnsignedTxInput>,
    424         outputs: Vec<TxOutput>,
    425         fee: Amount,
    426     },
    427     Burn {
    428         inputs: Vec<UnsignedTxInput>,
    429         change: Vec<TxOutput>,
    430         amount: Amount,
    431         fee: Amount,
    432     },
    433 }
    434 
    435 impl UnsignedUtxoTransaction {
    436     pub(super) fn sign(self, wallet: &Wallet) -> Transaction {
    437         let signature = wallet.sign_payload(&self.canonical());
    438         let signed_inputs = self
    439             .inputs()
    440             .iter()
    441             .map(|input| TxInput {
    442                 outpoint: input.outpoint.clone(),
    443                 owner: input.owner.clone(),
    444                 signature: signature.clone(),
    445             })
    446             .collect::<Vec<_>>();
    447         match self {
    448             Self::Transfer { outputs, fee, .. } => Transaction::Transfer {
    449                 inputs: signed_inputs,
    450                 outputs,
    451                 fee,
    452                 signature,
    453             },
    454             Self::Burn {
    455                 change,
    456                 amount,
    457                 fee,
    458                 ..
    459             } => Transaction::Burn {
    460                 inputs: signed_inputs,
    461                 change,
    462                 amount,
    463                 fee,
    464                 signature,
    465             },
    466         }
    467     }
    468 
    469     fn inputs(&self) -> &[UnsignedTxInput] {
    470         match self {
    471             Self::Transfer { inputs, .. } | Self::Burn { inputs, .. } => inputs,
    472         }
    473     }
    474 
    475     pub(super) fn canonical(&self) -> String {
    476         match self {
    477             Self::Transfer {
    478                 inputs,
    479                 outputs,
    480                 fee,
    481             } => format!(
    482                 "utxo-transfer:{}:{}:{fee}",
    483                 canonical_inputs(inputs),
    484                 canonical_outputs(outputs)
    485             ),
    486             Self::Burn {
    487                 inputs,
    488                 change,
    489                 amount,
    490                 fee,
    491             } => format!(
    492                 "utxo-burn:{}:{}:{amount}:{fee}",
    493                 canonical_inputs(inputs),
    494                 canonical_outputs(change)
    495             ),
    496         }
    497     }
    498 }
    499 
    500 pub(super) fn unsigned_inputs(inputs: &[TxInput]) -> Vec<UnsignedTxInput> {
    501     inputs.iter().map(TxInput::without_signature).collect()
    502 }
    503 
    504 pub(super) fn signed_blinded_inputs(inputs: &[UnsignedTxInput], signature: &str) -> Vec<TxInput> {
    505     inputs
    506         .iter()
    507         .map(|input| TxInput {
    508             outpoint: input.outpoint.clone(),
    509             owner: input.owner.clone(),
    510             signature: signature.to_string(),
    511         })
    512         .collect()
    513 }
    514 
    515 pub(super) fn canonical_inputs(inputs: &[UnsignedTxInput]) -> String {
    516     inputs
    517         .iter()
    518         .map(|input| {
    519             format!(
    520                 "{}:{}:{}",
    521                 input.outpoint.txid, input.outpoint.index, input.owner
    522             )
    523         })
    524         .collect::<Vec<_>>()
    525         .join("|")
    526 }
    527 
    528 pub(super) fn canonical_signed_inputs(inputs: &[TxInput]) -> String {
    529     inputs
    530         .iter()
    531         .map(|input| {
    532             format!(
    533                 "{}:{}:{}:{}",
    534                 input.outpoint.txid, input.outpoint.index, input.owner, input.signature
    535             )
    536         })
    537         .collect::<Vec<_>>()
    538         .join("|")
    539 }
    540 
    541 fn canonical_outputs(outputs: &[TxOutput]) -> String {
    542     outputs
    543         .iter()
    544         .map(|output| format!("{}:{}", output.address, output.amount))
    545         .collect::<Vec<_>>()
    546         .join("|")
    547 }
    548 
    549 fn pending_spent_outpoints(pending: &[Transaction]) -> BTreeSet<OutPoint> {
    550     pending
    551         .iter()
    552         .flat_map(|tx| tx.inputs().iter().map(|input| input.outpoint.clone()))
    553         .collect()
    554 }
    555 
    556 pub(super) fn transaction_inputs_spent_by(
    557     transaction: &Transaction,
    558     pending: &[Transaction],
    559 ) -> bool {
    560     let spent = pending_spent_outpoints(pending);
    561     transaction
    562         .inputs()
    563         .iter()
    564         .any(|input| spent.contains(&input.outpoint))
    565 }
    566 
    567 pub(super) fn transaction_inputs_spent_by_inputs(
    568     inputs: &[TxInput],
    569     pending: &[Transaction],
    570 ) -> bool {
    571     let spent = pending_spent_outpoints(pending);
    572     inputs.iter().any(|input| spent.contains(&input.outpoint))
    573 }
    574 
    575 pub(super) fn blinded_transaction_inputs_spent_by(
    576     transaction: &BlindedTransaction,
    577     pending: &[BlindedTransaction],
    578 ) -> bool {
    579     let spent = pending
    580         .iter()
    581         .flat_map(|transaction| {
    582             transaction
    583                 .inputs
    584                 .iter()
    585                 .map(|input| input.outpoint.clone())
    586         })
    587         .collect::<BTreeSet<_>>();
    588     transaction
    589         .inputs
    590         .iter()
    591         .any(|input| spent.contains(&input.outpoint))
    592 }
    593 
    594 pub(super) fn transaction_inputs_available(
    595     transaction: &Transaction,
    596     utxos: &BTreeMap<OutPoint, TxOutput>,
    597 ) -> bool {
    598     transaction
    599         .inputs()
    600         .iter()
    601         .all(|input| utxos.contains_key(&input.outpoint))
    602 }
    603 
    604 pub(super) fn blinded_transaction_inputs_available(
    605     transaction: &BlindedTransaction,
    606     utxos: &BTreeMap<OutPoint, TxOutput>,
    607 ) -> bool {
    608     transaction
    609         .inputs
    610         .iter()
    611         .all(|input| utxos.contains_key(&input.outpoint))
    612 }