iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

transaction.rs (25956B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use anyhow::{Context, Result, bail};
      4 use serde::{Deserialize, Serialize};
      5 
      6 use super::validation::{decode_canonical_hex, decode_canonical_hex_array};
      7 use super::{
      8     AddressNetwork, Amount, HASH_BYTES, MINE_FINALIZER_FEE, MINE_REWARD, PUBLIC_KEY_BYTES,
      9     SIGNATURE_BYTES, Wallet, canonical_transaction_size_bytes, decode_hex_array,
     10     decode_versioned_address, genesis_allocation_outpoint, hash_meets_difficulty, hex_encode,
     11     hex_hash, mine_payload, mine_signature, stratum_mine_header_bytes, stratum_mine_signature,
     12     verify_ed25519,
     13 };
     14 
     15 pub const TRANSACTION_SIGNING_FORMAT_VERSION: u16 = 1;
     16 
     17 #[derive(Clone, Debug, Eq, PartialEq)]
     18 pub(super) struct TransactionSigningDomain {
     19     chain_id: String,
     20     genesis_hash: String,
     21     chain_bound: bool,
     22 }
     23 
     24 impl TransactionSigningDomain {
     25     pub(super) fn new(chain_id: impl Into<String>, genesis_hash: impl Into<String>) -> Self {
     26         Self {
     27             chain_id: chain_id.into(),
     28             genesis_hash: genesis_hash.into(),
     29             chain_bound: true,
     30         }
     31     }
     32 
     33     pub(super) fn legacy() -> Self {
     34         Self {
     35             chain_id: String::new(),
     36             genesis_hash: String::new(),
     37             chain_bound: false,
     38         }
     39     }
     40 
     41     pub(super) fn for_height(
     42         chain_id: impl Into<String>,
     43         genesis_hash: impl Into<String>,
     44         height: u64,
     45     ) -> Self {
     46         if height >= super::TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT {
     47             Self::new(chain_id, genesis_hash)
     48         } else {
     49             Self::legacy()
     50         }
     51     }
     52 
     53     pub(super) fn is_chain_bound(&self) -> bool {
     54         self.chain_bound
     55     }
     56 
     57     pub(super) fn encode(&self, bytes: &mut Vec<u8>) -> Result<()> {
     58         if !self.chain_bound {
     59             bail!("legacy transaction signing has no binary chain domain");
     60         }
     61         bytes.extend_from_slice(b"IUNA-TX");
     62         bytes.extend_from_slice(&TRANSACTION_SIGNING_FORMAT_VERSION.to_be_bytes());
     63         encode_bytes(bytes, self.chain_id.as_bytes(), "chain ID")?;
     64         let genesis_hash = decode_canonical_hex_array::<HASH_BYTES>(&self.genesis_hash)
     65             .context("transaction signing genesis hash is invalid")?;
     66         encode_bytes(bytes, &genesis_hash, "genesis hash")
     67     }
     68 }
     69 
     70 #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
     71 pub struct OutPoint {
     72     pub txid: String,
     73     pub index: u32,
     74 }
     75 
     76 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     77 pub struct TxInput {
     78     pub outpoint: OutPoint,
     79     pub owner: String,
     80     pub signature: String,
     81 }
     82 
     83 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     84 pub struct TxOutput {
     85     pub address: String,
     86     pub amount: Amount,
     87 }
     88 
     89 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
     90 #[serde(tag = "kind", rename_all = "snake_case")]
     91 pub enum Transaction {
     92     Transfer {
     93         inputs: Vec<TxInput>,
     94         outputs: Vec<TxOutput>,
     95         #[serde(default)]
     96         fee: Amount,
     97         signature: String,
     98     },
     99     Burn {
    100         inputs: Vec<TxInput>,
    101         change: Vec<TxOutput>,
    102         amount: Amount,
    103         #[serde(default)]
    104         fee: Amount,
    105         #[serde(default, skip_serializing_if = "Option::is_none")]
    106         anchor: Option<String>,
    107         signature: String,
    108     },
    109     Mine {
    110         recipient: String,
    111         anchor: String,
    112         #[serde(default)]
    113         salt: u64,
    114         nonce: u64,
    115         difficulty_bits: u32,
    116         #[serde(default, skip_serializing_if = "Option::is_none")]
    117         proof_header: Option<String>,
    118         signature: String,
    119     },
    120 }
    121 
    122 #[derive(Clone, Debug, Eq, PartialEq)]
    123 pub struct MineSearchOutcome {
    124     pub transaction: Option<Transaction>,
    125     pub next_nonce: u64,
    126     pub attempts: u64,
    127 }
    128 
    129 impl Transaction {
    130     pub fn genesis_burn(from: impl Into<String>, amount: Amount) -> Self {
    131         let from = from.into();
    132         Self::genesis_burn_with_change(from, amount, Vec::new())
    133     }
    134 
    135     pub(super) fn genesis_burn_with_allocation(
    136         from: impl Into<String>,
    137         amount: Amount,
    138         allocation: Amount,
    139     ) -> Result<Self> {
    140         if amount > allocation {
    141             bail!("genesis burn exceeds allocation");
    142         }
    143         let from = from.into();
    144         let change_amount = allocation - amount;
    145         let change = if change_amount > 0 {
    146             vec![TxOutput {
    147                 address: from.clone(),
    148                 amount: change_amount,
    149             }]
    150         } else {
    151             Vec::new()
    152         };
    153         Ok(Self::genesis_burn_with_change(from, amount, change))
    154     }
    155 
    156     fn genesis_burn_with_change(from: String, amount: Amount, change: Vec<TxOutput>) -> Self {
    157         let input = TxInput {
    158             outpoint: genesis_allocation_outpoint(&from),
    159             owner: from.clone(),
    160             signature: "genesis".to_string(),
    161         };
    162         let unsigned = UnsignedUtxoTransaction::Burn {
    163             inputs: vec![input.without_signature()],
    164             change: change.clone(),
    165             amount,
    166             fee: 0,
    167             anchor: None,
    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             anchor: None,
    176             signature,
    177         }
    178     }
    179 
    180     pub fn sender(&self) -> &str {
    181         match self {
    182             Self::Transfer { inputs, .. } | Self::Burn { inputs, .. } => inputs
    183                 .first()
    184                 .map(|input| input.owner.as_str())
    185                 .unwrap_or(""),
    186             Self::Mine { recipient, .. } => recipient.as_str(),
    187         }
    188     }
    189 
    190     pub fn to(&self) -> Option<&str> {
    191         match self {
    192             Self::Transfer { outputs, .. } => outputs.first().map(|output| output.address.as_str()),
    193             Self::Burn { .. } => None,
    194             Self::Mine { recipient, .. } => Some(recipient.as_str()),
    195         }
    196     }
    197 
    198     pub fn amount(&self) -> Amount {
    199         match self {
    200             Self::Transfer { outputs, .. } => {
    201                 outputs.first().map(|output| output.amount).unwrap_or(0)
    202             }
    203             Self::Burn { amount, .. } => *amount,
    204             Self::Mine { .. } => MINE_REWARD,
    205         }
    206     }
    207 
    208     pub fn fee(&self) -> Amount {
    209         match self {
    210             Self::Transfer { fee, .. } | Self::Burn { fee, .. } => *fee,
    211             Self::Mine { .. } => MINE_FINALIZER_FEE,
    212         }
    213     }
    214 
    215     pub fn total_debit(&self) -> Result<Amount> {
    216         if matches!(self, Self::Mine { .. }) {
    217             return Ok(0);
    218         }
    219         self.amount()
    220             .checked_add(self.fee())
    221             .context("transaction amount plus fee overflows")
    222     }
    223 
    224     pub fn signature(&self) -> &str {
    225         match self {
    226             Self::Transfer { signature, .. } | Self::Burn { signature, .. } => signature,
    227             Self::Mine { signature, .. } => signature,
    228         }
    229     }
    230 
    231     pub fn is_burn(&self) -> bool {
    232         matches!(self, Self::Burn { .. })
    233     }
    234 
    235     pub(super) fn burn_anchor(&self) -> Option<&str> {
    236         match self {
    237             Self::Burn { anchor, .. } => anchor.as_deref(),
    238             Self::Transfer { .. } | Self::Mine { .. } => None,
    239         }
    240     }
    241 
    242     pub fn canonical(&self) -> String {
    243         format!("{}:{}", self.signing_payload(), self.signature())
    244     }
    245 
    246     pub fn economic_size_bytes(&self) -> usize {
    247         canonical_transaction_size_bytes(self)
    248     }
    249 
    250     pub fn serialized_size_bytes(&self) -> Result<usize> {
    251         serde_json::to_vec(self)
    252             .map(|bytes| bytes.len())
    253             .context("failed to serialize transaction for size check")
    254     }
    255 
    256     fn signing_payload(&self) -> String {
    257         match self {
    258             Self::Transfer {
    259                 inputs,
    260                 outputs,
    261                 fee,
    262                 ..
    263             } => UnsignedUtxoTransaction::Transfer {
    264                 inputs: unsigned_inputs(inputs),
    265                 outputs: outputs.clone(),
    266                 fee: *fee,
    267             }
    268             .canonical(),
    269             Self::Burn {
    270                 inputs,
    271                 change,
    272                 amount,
    273                 fee,
    274                 anchor,
    275                 ..
    276             } => UnsignedUtxoTransaction::Burn {
    277                 inputs: unsigned_inputs(inputs),
    278                 change: change.clone(),
    279                 amount: *amount,
    280                 fee: *fee,
    281                 anchor: anchor.clone(),
    282             }
    283             .canonical(),
    284             Self::Mine {
    285                 recipient,
    286                 anchor,
    287                 salt,
    288                 nonce,
    289                 difficulty_bits,
    290                 ..
    291             } => mine_payload(recipient, anchor, *salt, *nonce, *difficulty_bits),
    292         }
    293     }
    294 
    295     pub(super) fn verify_signature(&self, domain: &TransactionSigningDomain) -> Result<()> {
    296         if let Self::Mine {
    297             recipient,
    298             anchor,
    299             salt,
    300             nonce,
    301             difficulty_bits,
    302             proof_header,
    303             signature,
    304         } = self
    305         {
    306             let expected = if let Some(proof_header) = proof_header {
    307                 let header = stratum_mine_header_bytes(
    308                     domain,
    309                     recipient,
    310                     anchor,
    311                     *salt,
    312                     *nonce,
    313                     *difficulty_bits,
    314                 )?;
    315                 let expected_header = hex_encode(header);
    316                 if *proof_header != expected_header {
    317                     bail!("mine transaction proof header is invalid");
    318                 }
    319                 stratum_mine_signature(&header)
    320             } else {
    321                 mine_signature(domain, recipient, anchor, *salt, *nonce, *difficulty_bits)?
    322             };
    323             if *signature != expected {
    324                 bail!("mine transaction proof hash is invalid");
    325             }
    326             if !hash_meets_difficulty(signature, *difficulty_bits) {
    327                 bail!("mine transaction proof does not meet difficulty");
    328             }
    329             return Ok(());
    330         }
    331         if self.signature().starts_with("iuna-genesis-burn:") || self.inputs_are_genesis_signed() {
    332             return Ok(());
    333         }
    334         if !self
    335             .inputs()
    336             .iter()
    337             .all(|input| input.signature == self.signature())
    338         {
    339             bail!("transaction input signature does not match transaction signature");
    340         }
    341         let sender = self.sender();
    342         let public_key = if domain.is_chain_bound() {
    343             decode_canonical_hex_array::<PUBLIC_KEY_BYTES>(sender)
    344         } else {
    345             decode_hex_array::<PUBLIC_KEY_BYTES>(sender)
    346         }
    347         .with_context(|| format!("invalid public key for {sender}"))?;
    348         let signature = if domain.is_chain_bound() {
    349             decode_canonical_hex_array::<SIGNATURE_BYTES>(self.signature())
    350         } else {
    351             decode_hex_array::<SIGNATURE_BYTES>(self.signature())
    352         }
    353         .context("invalid signature hex")?;
    354         let signing_bytes = if domain.is_chain_bound() {
    355             self.signing_bytes(domain)?
    356         } else {
    357             self.signing_payload().into_bytes()
    358         };
    359         verify_ed25519(&public_key, &signing_bytes, &signature, "transaction")
    360     }
    361 
    362     pub(super) fn inputs(&self) -> &[TxInput] {
    363         match self {
    364             Self::Transfer { inputs, .. } | Self::Burn { inputs, .. } => inputs,
    365             Self::Mine { .. } => &[],
    366         }
    367     }
    368 
    369     pub(super) fn outputs(&self) -> Vec<TxOutput> {
    370         match self {
    371             Self::Transfer { outputs, .. } => outputs.clone(),
    372             Self::Burn { change, .. } => change.clone(),
    373             Self::Mine { recipient, .. } => vec![TxOutput {
    374                 address: recipient.clone(),
    375                 amount: MINE_REWARD,
    376             }],
    377         }
    378     }
    379 
    380     fn inputs_are_genesis_signed(&self) -> bool {
    381         self.inputs()
    382             .iter()
    383             .all(|input| input.signature == "genesis")
    384     }
    385 
    386     fn signing_bytes(&self, domain: &TransactionSigningDomain) -> Result<Vec<u8>> {
    387         match self {
    388             Self::Transfer {
    389                 inputs,
    390                 outputs,
    391                 fee,
    392                 ..
    393             } => UnsignedUtxoTransaction::Transfer {
    394                 inputs: unsigned_inputs(inputs),
    395                 outputs: outputs.clone(),
    396                 fee: *fee,
    397             }
    398             .signing_bytes(domain),
    399             Self::Burn {
    400                 inputs,
    401                 change,
    402                 amount,
    403                 fee,
    404                 anchor,
    405                 ..
    406             } => UnsignedUtxoTransaction::Burn {
    407                 inputs: unsigned_inputs(inputs),
    408                 change: change.clone(),
    409                 amount: *amount,
    410                 fee: *fee,
    411                 anchor: anchor.clone(),
    412             }
    413             .signing_bytes(domain),
    414             Self::Mine { .. } => unreachable!("mine transactions use proof hashes"),
    415         }
    416     }
    417 }
    418 
    419 impl TxInput {
    420     pub(super) fn without_signature(&self) -> UnsignedTxInput {
    421         UnsignedTxInput {
    422             outpoint: self.outpoint.clone(),
    423             owner: self.owner.clone(),
    424         }
    425     }
    426 }
    427 
    428 impl OutPoint {
    429     pub(super) fn id(&self) -> String {
    430         format!("{}:{}", self.txid, self.index)
    431     }
    432 }
    433 
    434 #[derive(Clone, Debug, Eq, PartialEq)]
    435 pub(super) struct UnsignedTxInput {
    436     pub(super) outpoint: OutPoint,
    437     pub(super) owner: String,
    438 }
    439 
    440 #[derive(Clone, Debug, Eq, PartialEq)]
    441 pub(super) enum UnsignedUtxoTransaction {
    442     Transfer {
    443         inputs: Vec<UnsignedTxInput>,
    444         outputs: Vec<TxOutput>,
    445         fee: Amount,
    446     },
    447     Burn {
    448         inputs: Vec<UnsignedTxInput>,
    449         change: Vec<TxOutput>,
    450         amount: Amount,
    451         fee: Amount,
    452         anchor: Option<String>,
    453     },
    454 }
    455 
    456 impl UnsignedUtxoTransaction {
    457     pub(super) fn sign(
    458         self,
    459         wallet: &Wallet,
    460         domain: &TransactionSigningDomain,
    461     ) -> Result<Transaction> {
    462         let signature = if domain.is_chain_bound() {
    463             wallet.sign_bytes(&self.signing_bytes(domain)?)
    464         } else {
    465             wallet.sign_payload(&self.canonical())
    466         };
    467         let signed_inputs = self
    468             .inputs()
    469             .iter()
    470             .map(|input| TxInput {
    471                 outpoint: input.outpoint.clone(),
    472                 owner: input.owner.clone(),
    473                 signature: signature.clone(),
    474             })
    475             .collect::<Vec<_>>();
    476         Ok(match self {
    477             Self::Transfer { outputs, fee, .. } => Transaction::Transfer {
    478                 inputs: signed_inputs,
    479                 outputs,
    480                 fee,
    481                 signature,
    482             },
    483             Self::Burn {
    484                 change,
    485                 amount,
    486                 fee,
    487                 anchor,
    488                 ..
    489             } => Transaction::Burn {
    490                 inputs: signed_inputs,
    491                 change,
    492                 amount,
    493                 fee,
    494                 anchor,
    495                 signature,
    496             },
    497         })
    498     }
    499 
    500     fn inputs(&self) -> &[UnsignedTxInput] {
    501         match self {
    502             Self::Transfer { inputs, .. } | Self::Burn { inputs, .. } => inputs,
    503         }
    504     }
    505 
    506     pub(super) fn canonical(&self) -> String {
    507         match self {
    508             Self::Transfer {
    509                 inputs,
    510                 outputs,
    511                 fee,
    512             } => format!(
    513                 "utxo-transfer:{}:{}:{fee}",
    514                 canonical_inputs(inputs),
    515                 canonical_outputs(outputs)
    516             ),
    517             Self::Burn {
    518                 inputs,
    519                 change,
    520                 amount,
    521                 fee,
    522                 anchor,
    523             } => match anchor {
    524                 Some(anchor) => format!(
    525                     "utxo-burn-v2:{}:{}:{amount}:{fee}:{anchor}",
    526                     canonical_inputs(inputs),
    527                     canonical_outputs(change)
    528                 ),
    529                 None => format!(
    530                     "utxo-burn:{}:{}:{amount}:{fee}",
    531                     canonical_inputs(inputs),
    532                     canonical_outputs(change)
    533                 ),
    534             },
    535         }
    536     }
    537 
    538     pub(super) fn signing_bytes(&self, domain: &TransactionSigningDomain) -> Result<Vec<u8>> {
    539         let mut bytes = Vec::new();
    540         domain.encode(&mut bytes)?;
    541         match self {
    542             Self::Transfer {
    543                 inputs,
    544                 outputs,
    545                 fee,
    546             } => {
    547                 bytes.push(1);
    548                 encode_inputs(&mut bytes, inputs)?;
    549                 encode_outputs(&mut bytes, outputs)?;
    550                 bytes.extend_from_slice(&fee.to_be_bytes());
    551             }
    552             Self::Burn {
    553                 inputs,
    554                 change,
    555                 amount,
    556                 fee,
    557                 anchor,
    558             } => {
    559                 bytes.push(if anchor.is_some() { 3 } else { 2 });
    560                 encode_inputs(&mut bytes, inputs)?;
    561                 encode_outputs(&mut bytes, change)?;
    562                 bytes.extend_from_slice(&amount.to_be_bytes());
    563                 bytes.extend_from_slice(&fee.to_be_bytes());
    564                 if let Some(anchor) = anchor {
    565                     let anchor = decode_canonical_hex_array::<HASH_BYTES>(anchor)
    566                         .context("burn anchor is invalid")?;
    567                     encode_bytes(&mut bytes, &anchor, "burn anchor")?;
    568                 }
    569             }
    570         }
    571         Ok(bytes)
    572     }
    573 }
    574 
    575 fn encode_inputs(bytes: &mut Vec<u8>, inputs: &[UnsignedTxInput]) -> Result<()> {
    576     encode_len(bytes, inputs.len(), "input count")?;
    577     for input in inputs {
    578         let txid = decode_canonical_hex(&input.outpoint.txid)
    579             .context("transaction input txid is invalid")?;
    580         if txid.len() != HASH_BYTES && txid.len() != SIGNATURE_BYTES {
    581             bail!("transaction input txid must be a hash or signature");
    582         }
    583         encode_bytes(bytes, &txid, "input txid")?;
    584         bytes.extend_from_slice(&input.outpoint.index.to_be_bytes());
    585         let owner = decode_canonical_hex_array::<PUBLIC_KEY_BYTES>(&input.owner)
    586             .context("transaction input owner is invalid")?;
    587         encode_bytes(bytes, &owner, "input owner")?;
    588     }
    589     Ok(())
    590 }
    591 
    592 fn encode_outputs(bytes: &mut Vec<u8>, outputs: &[TxOutput]) -> Result<()> {
    593     encode_len(bytes, outputs.len(), "output count")?;
    594     for output in outputs {
    595         let address = decode_canonical_hex_array::<PUBLIC_KEY_BYTES>(&output.address)
    596             .context("transaction output address is invalid")?;
    597         encode_bytes(bytes, &address, "output address")?;
    598         bytes.extend_from_slice(&output.amount.to_be_bytes());
    599     }
    600     Ok(())
    601 }
    602 
    603 fn encode_bytes(bytes: &mut Vec<u8>, value: &[u8], label: &str) -> Result<()> {
    604     encode_len(bytes, value.len(), label)?;
    605     bytes.extend_from_slice(value);
    606     Ok(())
    607 }
    608 
    609 fn encode_len(bytes: &mut Vec<u8>, len: usize, label: &str) -> Result<()> {
    610     let len = u32::try_from(len).with_context(|| format!("{label} exceeds u32 length"))?;
    611     bytes.extend_from_slice(&len.to_be_bytes());
    612     Ok(())
    613 }
    614 
    615 pub(super) fn mine_signing_bytes(
    616     domain: &TransactionSigningDomain,
    617     recipient: &str,
    618     anchor: &str,
    619     salt: u64,
    620     nonce: u64,
    621     difficulty_bits: u32,
    622 ) -> Result<Vec<u8>> {
    623     let mut bytes = Vec::new();
    624     domain.encode(&mut bytes)?;
    625     bytes.push(3);
    626     if let Ok(recipient) = decode_canonical_hex_array::<PUBLIC_KEY_BYTES>(recipient) {
    627         encode_bytes(&mut bytes, &recipient, "mine recipient")?;
    628     } else {
    629         let network = if recipient.starts_with("tiuna1") {
    630             AddressNetwork::Testnet
    631         } else {
    632             AddressNetwork::Mainnet
    633         };
    634         let recipient =
    635             decode_versioned_address(recipient, network).context("mine recipient is invalid")?;
    636         let mut encoded = vec![recipient.version.wire_id()];
    637         encoded.extend_from_slice(&recipient.payload);
    638         encode_bytes(&mut bytes, &encoded, "mine recipient")?;
    639     }
    640     let anchor =
    641         decode_canonical_hex_array::<HASH_BYTES>(anchor).context("mine anchor is invalid")?;
    642     encode_bytes(&mut bytes, &anchor, "mine anchor")?;
    643     bytes.extend_from_slice(&salt.to_be_bytes());
    644     bytes.extend_from_slice(&nonce.to_be_bytes());
    645     bytes.extend_from_slice(&difficulty_bits.to_be_bytes());
    646     Ok(bytes)
    647 }
    648 
    649 pub(super) fn unsigned_inputs(inputs: &[TxInput]) -> Vec<UnsignedTxInput> {
    650     inputs.iter().map(TxInput::without_signature).collect()
    651 }
    652 
    653 pub(super) fn canonical_inputs(inputs: &[UnsignedTxInput]) -> String {
    654     inputs
    655         .iter()
    656         .map(|input| {
    657             format!(
    658                 "{}:{}:{}",
    659                 input.outpoint.txid, input.outpoint.index, input.owner
    660             )
    661         })
    662         .collect::<Vec<_>>()
    663         .join("|")
    664 }
    665 
    666 fn canonical_outputs(outputs: &[TxOutput]) -> String {
    667     outputs
    668         .iter()
    669         .map(|output| format!("{}:{}", output.address, output.amount))
    670         .collect::<Vec<_>>()
    671         .join("|")
    672 }
    673 
    674 fn pending_spent_outpoints(pending: &[Transaction]) -> BTreeSet<OutPoint> {
    675     pending
    676         .iter()
    677         .flat_map(|tx| tx.inputs().iter().map(|input| input.outpoint.clone()))
    678         .collect()
    679 }
    680 
    681 pub(super) fn transaction_inputs_spent_by(
    682     transaction: &Transaction,
    683     pending: &[Transaction],
    684 ) -> bool {
    685     let spent = pending_spent_outpoints(pending);
    686     transaction
    687         .inputs()
    688         .iter()
    689         .any(|input| spent.contains(&input.outpoint))
    690 }
    691 
    692 pub(super) fn transaction_inputs_available(
    693     transaction: &Transaction,
    694     utxos: &BTreeMap<OutPoint, TxOutput>,
    695 ) -> bool {
    696     transaction
    697         .inputs()
    698         .iter()
    699         .all(|input| utxos.contains_key(&input.outpoint))
    700 }
    701 
    702 #[cfg(test)]
    703 mod tests {
    704     use super::*;
    705 
    706     #[test]
    707     fn signing_domain_switches_exactly_at_height_1000() {
    708         let before = TransactionSigningDomain::for_height(
    709             "activation-chain",
    710             "11".repeat(32),
    711             crate::domain::TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT - 1,
    712         );
    713         let activated = TransactionSigningDomain::for_height(
    714             "activation-chain",
    715             "11".repeat(32),
    716             crate::domain::TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT,
    717         );
    718 
    719         assert!(!before.is_chain_bound());
    720         assert!(activated.is_chain_bound());
    721     }
    722 
    723     #[test]
    724     fn signing_format_v1_has_a_stable_typed_binary_vector() {
    725         let transaction = UnsignedUtxoTransaction::Transfer {
    726             inputs: vec![UnsignedTxInput {
    727                 outpoint: OutPoint {
    728                     txid: "11".repeat(32),
    729                     index: 7,
    730                 },
    731                 owner: "33".repeat(32),
    732             }],
    733             outputs: vec![TxOutput {
    734                 address: "44".repeat(32),
    735                 amount: 5,
    736             }],
    737             fee: 1,
    738         };
    739         let domain = TransactionSigningDomain::new("iuna-test-vector", "22".repeat(32));
    740 
    741         let encoded = hex_encode(transaction.signing_bytes(&domain).unwrap());
    742 
    743         assert_eq!(
    744             encoded,
    745             concat!(
    746                 "49554e412d5458", // IUNA-TX domain tag
    747                 "0001",           // signing format version
    748                 "00000010",
    749                 "69756e612d746573742d766563746f72", // chain ID
    750                 "00000020",
    751                 "2222222222222222222222222222222222222222222222222222222222222222", // genesis hash
    752                 "01",                                                               // transfer type
    753                 "00000001",                                                         // input count
    754                 "00000020",
    755                 "1111111111111111111111111111111111111111111111111111111111111111", // txid
    756                 "00000007",                                                         // output index
    757                 "00000020",
    758                 "3333333333333333333333333333333333333333333333333333333333333333", // owner
    759                 "00000001",                                                         // output count
    760                 "00000020",
    761                 "4444444444444444444444444444444444444444444444444444444444444444", // address
    762                 "0000000000000005",                                                 // amount
    763                 "0000000000000001",                                                 // fee
    764             )
    765         );
    766     }
    767 
    768     #[test]
    769     fn legacy_text_signature_is_invalid_under_format_v1() {
    770         let wallet = Wallet::from_seed("legacy-transaction-signature");
    771         let unsigned = UnsignedUtxoTransaction::Transfer {
    772             inputs: vec![UnsignedTxInput {
    773                 outpoint: OutPoint {
    774                     txid: "11".repeat(32),
    775                     index: 0,
    776                 },
    777                 owner: wallet.address().to_string(),
    778             }],
    779             outputs: vec![TxOutput {
    780                 address: wallet.address().to_string(),
    781                 amount: 9,
    782             }],
    783             fee: 1,
    784         };
    785         let legacy_signature = wallet.sign_payload(&unsigned.canonical());
    786         let transaction = Transaction::Transfer {
    787             inputs: vec![TxInput {
    788                 outpoint: OutPoint {
    789                     txid: "11".repeat(32),
    790                     index: 0,
    791                 },
    792                 owner: wallet.address().to_string(),
    793                 signature: legacy_signature.clone(),
    794             }],
    795             outputs: vec![TxOutput {
    796                 address: wallet.address().to_string(),
    797                 amount: 9,
    798             }],
    799             fee: 1,
    800             signature: legacy_signature,
    801         };
    802         let domain = TransactionSigningDomain::new("iuna-mainnet-v1", "22".repeat(32));
    803 
    804         assert!(transaction.verify_signature(&domain).is_err());
    805         transaction
    806             .verify_signature(&TransactionSigningDomain::legacy())
    807             .unwrap();
    808     }
    809 }