iuna

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

ledger_builders.rs (13526B)


      1 use anyhow::{Context, Result, anyhow, bail};
      2 use getrandom::getrandom;
      3 
      4 use super::blinded::{
      5     blinded_envelope_fee_for_transaction, blinded_payload_from_transaction,
      6     blinded_transaction_commitment, blinded_transaction_signing_payload, encrypt_blinded_payload,
      7 };
      8 use super::hex::{hex_encode, hex_hash};
      9 use super::mining::mine_signature;
     10 use super::stratum::{
     11     hash_meets_difficulty, stratum_mine_header_bytes, stratum_mine_signature, stratum_mine_template,
     12 };
     13 use super::transaction::{
     14     UnsignedTxInput, UnsignedUtxoTransaction, signed_blinded_inputs, unsigned_inputs,
     15 };
     16 use super::validation::validate_address;
     17 use super::{
     18     Amount, BLINDED_KEY_BYTES, BLINDED_NONCE_BYTES, BlindedReveal, BlindedTransaction,
     19     BuiltBlindedTransaction, Ledger, MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS, MineSearchOutcome,
     20     OutPoint, StratumMineShare, StratumMineTemplate, Transaction, TxOutput, Wallet,
     21 };
     22 
     23 impl Ledger {
     24     pub fn build_transfer(
     25         &self,
     26         wallet: &Wallet,
     27         to: impl Into<String>,
     28         amount: Amount,
     29         fee: Amount,
     30     ) -> Result<Transaction> {
     31         let to = to.into();
     32         validate_address(&to, "transfer recipient")?;
     33         let required = amount
     34             .checked_add(fee)
     35             .context("transfer amount plus fee overflows")?;
     36         let (inputs, input_total) = self.select_inputs(wallet.address(), required)?;
     37         let mut outputs = vec![TxOutput {
     38             address: to,
     39             amount,
     40         }];
     41         let change = input_total
     42             .checked_sub(required)
     43             .context("selected inputs do not cover transfer")?;
     44         if change > 0 {
     45             outputs.push(TxOutput {
     46                 address: wallet.address().to_string(),
     47                 amount: change,
     48             });
     49         }
     50         let transaction = UnsignedUtxoTransaction::Transfer {
     51             inputs,
     52             outputs,
     53             fee,
     54         }
     55         .sign(wallet);
     56         self.validate_new_transaction(&transaction)?;
     57         Ok(transaction)
     58     }
     59 
     60     pub fn build_transfer_with_inputs(
     61         &self,
     62         wallet: &Wallet,
     63         to: impl Into<String>,
     64         amount: Amount,
     65         fee: Amount,
     66         outpoints: &[OutPoint],
     67     ) -> Result<Transaction> {
     68         let to = to.into();
     69         validate_address(&to, "transfer recipient")?;
     70         let required = amount
     71             .checked_add(fee)
     72             .context("transfer amount plus fee overflows")?;
     73         let (inputs, input_total) =
     74             self.select_inputs_by_outpoint(wallet.address(), required, outpoints)?;
     75         let mut outputs = vec![TxOutput {
     76             address: to,
     77             amount,
     78         }];
     79         let change = input_total
     80             .checked_sub(required)
     81             .context("selected inputs do not cover transfer")?;
     82         if change > 0 {
     83             outputs.push(TxOutput {
     84                 address: wallet.address().to_string(),
     85                 amount: change,
     86             });
     87         }
     88         let transaction = UnsignedUtxoTransaction::Transfer {
     89             inputs,
     90             outputs,
     91             fee,
     92         }
     93         .sign(wallet);
     94         self.validate_new_transaction(&transaction)?;
     95         Ok(transaction)
     96     }
     97 
     98     pub fn build_burn(&self, wallet: &Wallet, amount: Amount, fee: Amount) -> Result<Transaction> {
     99         let required = amount
    100             .checked_add(fee)
    101             .context("burn amount plus fee overflows")?;
    102         let (inputs, input_total) = self.select_inputs(wallet.address(), required)?;
    103         self.build_burn_from_inputs(wallet, amount, fee, inputs, input_total)
    104     }
    105 
    106     pub fn build_burn_with_inputs(
    107         &self,
    108         wallet: &Wallet,
    109         amount: Amount,
    110         fee: Amount,
    111         outpoints: &[OutPoint],
    112     ) -> Result<Transaction> {
    113         let required = amount
    114             .checked_add(fee)
    115             .context("burn amount plus fee overflows")?;
    116         let (inputs, input_total) =
    117             self.select_inputs_by_outpoint(wallet.address(), required, outpoints)?;
    118         self.build_burn_from_inputs(wallet, amount, fee, inputs, input_total)
    119     }
    120 
    121     fn build_burn_from_inputs(
    122         &self,
    123         wallet: &Wallet,
    124         amount: Amount,
    125         fee: Amount,
    126         inputs: Vec<UnsignedTxInput>,
    127         input_total: Amount,
    128     ) -> Result<Transaction> {
    129         let required = amount
    130             .checked_add(fee)
    131             .context("burn amount plus fee overflows")?;
    132         let change_amount = input_total
    133             .checked_sub(required)
    134             .context("selected inputs do not cover burn")?;
    135         let change = if change_amount > 0 {
    136             vec![TxOutput {
    137                 address: wallet.address().to_string(),
    138                 amount: change_amount,
    139             }]
    140         } else {
    141             Vec::new()
    142         };
    143         let transaction = UnsignedUtxoTransaction::Burn {
    144             inputs,
    145             change,
    146             amount,
    147             fee,
    148         }
    149         .sign(wallet);
    150         self.validate_new_transaction(&transaction)?;
    151         Ok(transaction)
    152     }
    153 
    154     pub fn build_blinded_burn(
    155         &self,
    156         wallet: &Wallet,
    157         amount: Amount,
    158         fee: Amount,
    159         expires_at_height: u64,
    160     ) -> Result<BuiltBlindedTransaction> {
    161         let transaction = self.build_burn(wallet, amount, fee)?;
    162         self.blind_transaction(wallet, transaction, fee, expires_at_height)
    163     }
    164 
    165     pub fn build_blinded_transfer(
    166         &self,
    167         wallet: &Wallet,
    168         to: impl Into<String>,
    169         amount: Amount,
    170         fee: Amount,
    171         expires_at_height: u64,
    172     ) -> Result<BuiltBlindedTransaction> {
    173         let transaction = self.build_transfer(wallet, to, amount, fee)?;
    174         self.blind_transaction(wallet, transaction, fee, expires_at_height)
    175     }
    176 
    177     pub fn build_blinded_transaction(
    178         &self,
    179         wallet: &Wallet,
    180         transaction: Transaction,
    181         expires_at_height: u64,
    182     ) -> Result<BuiltBlindedTransaction> {
    183         if matches!(transaction, Transaction::Mine { .. }) {
    184             bail!("mine actions are public and cannot be blinded");
    185         }
    186         let fee = blinded_envelope_fee_for_transaction(&transaction);
    187         self.blind_transaction(wallet, transaction, fee, expires_at_height)
    188     }
    189 
    190     fn blind_transaction(
    191         &self,
    192         wallet: &Wallet,
    193         transaction: Transaction,
    194         fee: Amount,
    195         expires_at_height: u64,
    196     ) -> Result<BuiltBlindedTransaction> {
    197         if expires_at_height <= self.height() {
    198             bail!("blinded transaction expiry must be in the future");
    199         }
    200         if expires_at_height
    201             > self
    202                 .height()
    203                 .saturating_add(MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS)
    204         {
    205             bail!("blinded transaction expiry is too far in the future");
    206         }
    207         if fee != blinded_envelope_fee_for_transaction(&transaction) {
    208             bail!("blinded transaction fee must match plaintext transaction fee");
    209         }
    210         let unsigned_inputs = if transaction.inputs().is_empty() && transaction.fee() > 0 {
    211             self.select_inputs(wallet.address(), transaction.fee())?.0
    212         } else {
    213             unsigned_inputs(transaction.inputs())
    214         };
    215         if unsigned_inputs
    216             .iter()
    217             .any(|input| input.owner != wallet.address())
    218         {
    219             bail!("blinded transaction inputs must be owned by the signing wallet");
    220         }
    221         let blinded_payload = blinded_payload_from_transaction(&transaction)?;
    222         let plaintext = serde_json::to_vec(&blinded_payload)
    223             .context("failed to serialize transaction for blinded payload")?;
    224         let payload_hash = hex_hash(&plaintext);
    225         let unsigned_commit_inputs = signed_blinded_inputs(&unsigned_inputs, "");
    226         let payload = transaction;
    227         let mut key = [0_u8; BLINDED_KEY_BYTES];
    228         let mut nonce = [0_u8; BLINDED_NONCE_BYTES];
    229         getrandom(&mut key)
    230             .map_err(|error| anyhow!("failed to generate blinded transaction key: {error}"))?;
    231         getrandom(&mut nonce)
    232             .map_err(|error| anyhow!("failed to generate blinded transaction nonce: {error}"))?;
    233         let ciphertext = encrypt_blinded_payload(
    234             &key,
    235             &nonce,
    236             &unsigned_commit_inputs,
    237             fee,
    238             expires_at_height,
    239             &plaintext,
    240         )?;
    241         let encrypted_size = u32::try_from(ciphertext.len())
    242             .context("blinded transaction ciphertext is too large")?;
    243         let transaction = BlindedTransaction {
    244             commitment: String::new(),
    245             inputs: unsigned_commit_inputs,
    246             fee,
    247             encrypted_size,
    248             expires_at_height,
    249             nonce: hex_encode(nonce),
    250             ciphertext: hex_encode(&ciphertext),
    251             payload_hash,
    252         };
    253         let signature = wallet.sign_payload(&blinded_transaction_signing_payload(&transaction));
    254         let transaction = BlindedTransaction {
    255             inputs: signed_blinded_inputs(&unsigned_inputs, &signature),
    256             ..transaction
    257         };
    258         let commitment = blinded_transaction_commitment(&transaction)?;
    259         let transaction = BlindedTransaction {
    260             commitment: commitment.clone(),
    261             ..transaction
    262         };
    263         self.validate_blinded_transaction(&transaction)?;
    264         Ok(BuiltBlindedTransaction {
    265             payload,
    266             transaction,
    267             reveal: BlindedReveal {
    268                 commitment,
    269                 key: hex_encode(key),
    270             },
    271         })
    272     }
    273 
    274     pub fn build_mine(&self, recipient: impl Into<String>) -> Result<Transaction> {
    275         let recipient = recipient.into();
    276         validate_address(&recipient, "mine recipient")?;
    277         let anchor = self.tip().hash.clone();
    278         let salt = 1;
    279         let difficulty_bits = self.current_mine_difficulty_bits();
    280         for nonce in 0..u64::MAX {
    281             let signature = mine_signature(&recipient, &anchor, salt, nonce, difficulty_bits);
    282             if !hash_meets_difficulty(&signature, difficulty_bits) {
    283                 continue;
    284             }
    285             let transaction = Transaction::Mine {
    286                 recipient: recipient.clone(),
    287                 anchor: anchor.clone(),
    288                 salt,
    289                 nonce,
    290                 difficulty_bits,
    291                 proof_header: None,
    292                 signature,
    293             };
    294             if self.has_transaction(transaction.signature()) {
    295                 continue;
    296             }
    297             self.validate_new_transaction(&transaction)?;
    298             return Ok(transaction);
    299         }
    300         bail!("could not find valid mine proof");
    301     }
    302 
    303     pub fn search_mine(
    304         &self,
    305         recipient: impl Into<String>,
    306         salt: u64,
    307         start_nonce: u64,
    308         max_attempts: u64,
    309     ) -> Result<MineSearchOutcome> {
    310         let recipient = recipient.into();
    311         validate_address(&recipient, "mine recipient")?;
    312         let anchor = self.tip().hash.clone();
    313         let difficulty_bits = self.current_mine_difficulty_bits();
    314         let mut attempts = 0_u64;
    315         let mut nonce = start_nonce;
    316         while attempts < max_attempts {
    317             let signature = mine_signature(&recipient, &anchor, salt, nonce, difficulty_bits);
    318             attempts = attempts.saturating_add(1);
    319             let next_nonce = nonce.checked_add(1).unwrap_or(0);
    320             if hash_meets_difficulty(&signature, difficulty_bits) {
    321                 let transaction = Transaction::Mine {
    322                     recipient: recipient.clone(),
    323                     anchor: anchor.clone(),
    324                     salt,
    325                     nonce,
    326                     difficulty_bits,
    327                     proof_header: None,
    328                     signature,
    329                 };
    330                 if !self.has_transaction(transaction.signature()) {
    331                     self.validate_new_transaction(&transaction)?;
    332                     return Ok(MineSearchOutcome {
    333                         transaction: Some(transaction),
    334                         next_nonce,
    335                         attempts,
    336                     });
    337                 }
    338             }
    339             nonce = next_nonce;
    340         }
    341         Ok(MineSearchOutcome {
    342             transaction: None,
    343             next_nonce: nonce,
    344             attempts,
    345         })
    346     }
    347 
    348     pub fn stratum_mine_template(
    349         &self,
    350         recipient: impl Into<String>,
    351         anchor: impl AsRef<str>,
    352         salt: u64,
    353         difficulty_bits: u32,
    354     ) -> Result<StratumMineTemplate> {
    355         stratum_mine_template(recipient, anchor.as_ref(), salt, difficulty_bits)
    356     }
    357 
    358     pub fn build_stratum_mine(
    359         &self,
    360         template: StratumMineTemplate,
    361         share: StratumMineShare,
    362     ) -> Result<Transaction> {
    363         let nonce = super::stratum::pack_stratum_nonce(share.extranonce2, share.header_nonce);
    364         let header = stratum_mine_header_bytes(
    365             &template.recipient,
    366             &template.anchor,
    367             template.salt,
    368             nonce,
    369             template.difficulty_bits,
    370         )?;
    371         let transaction = Transaction::Mine {
    372             recipient: template.recipient,
    373             anchor: template.anchor,
    374             salt: template.salt,
    375             nonce,
    376             difficulty_bits: template.difficulty_bits,
    377             proof_header: Some(hex_encode(header)),
    378             signature: stratum_mine_signature(&header),
    379         };
    380         self.validate_new_transaction(&transaction)?;
    381         Ok(transaction)
    382     }
    383 }