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 }