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 }