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 }