validation.rs (9008B)
1 use anyhow::{Context, Result, bail}; 2 3 use super::{ 4 HASH_BYTES, PUBLIC_KEY_BYTES, SIGNATURE_BYTES, Transaction, TxInput, TxOutput, decode_hex, 5 decode_hex_array, hex_encode, stratum::STRATUM_MINE_HEADER_BYTES, 6 }; 7 8 pub fn validate_address(address: &str, label: &str) -> Result<()> { 9 decode_hex_array::<PUBLIC_KEY_BYTES>(address) 10 .with_context(|| format!("invalid {label} address"))?; 11 Ok(()) 12 } 13 14 pub(super) fn validate_hash(hash: &str, label: &str) -> Result<()> { 15 decode_hex_array::<HASH_BYTES>(hash).with_context(|| format!("invalid {label}"))?; 16 Ok(()) 17 } 18 19 pub(super) fn validate_signature(signature: &str, label: &str) -> Result<()> { 20 decode_hex_array::<SIGNATURE_BYTES>(signature).with_context(|| format!("invalid {label}"))?; 21 Ok(()) 22 } 23 24 pub(super) fn validate_stratum_header(header: &str) -> Result<()> { 25 decode_hex_array::<STRATUM_MINE_HEADER_BYTES>(header) 26 .context("invalid mine transaction proof header")?; 27 Ok(()) 28 } 29 30 pub(super) fn validate_protocol_id(value: &str, label: &str) -> Result<()> { 31 let bytes = decode_hex(value).with_context(|| format!("invalid {label}"))?; 32 match bytes.len() { 33 HASH_BYTES | SIGNATURE_BYTES => Ok(()), 34 length => bail!("invalid {label}: expected 32 or 64 bytes, got {length}"), 35 } 36 } 37 38 pub(super) fn decode_canonical_hex(value: &str) -> Result<Vec<u8>> { 39 let bytes = decode_hex(value)?; 40 if hex_encode(&bytes) != value { 41 bail!("hex must use canonical lowercase encoding"); 42 } 43 Ok(bytes) 44 } 45 46 pub(super) fn decode_canonical_hex_array<const N: usize>(value: &str) -> Result<[u8; N]> { 47 let bytes = decode_canonical_hex(value)?; 48 let len = bytes.len(); 49 bytes 50 .try_into() 51 .map_err(|_| anyhow::anyhow!("expected {N} hex bytes, got {len}")) 52 } 53 54 pub(super) fn canonical_transaction_size_bytes(transaction: &Transaction) -> usize { 55 match transaction { 56 Transaction::Transfer { 57 inputs, 58 outputs, 59 fee, 60 signature, 61 } => { 62 1 + compact_len(inputs.len() as u128) 63 + compact_inputs_size_bytes(inputs) 64 + compact_len(outputs.len() as u128) 65 + compact_outputs_size_bytes(outputs) 66 + compact_len(u128::from(*fee)) 67 + signature_size_bytes(signature) 68 } 69 Transaction::Burn { 70 inputs, 71 change, 72 amount, 73 fee, 74 anchor, 75 signature, 76 } => { 77 1 + compact_len(inputs.len() as u128) 78 + compact_inputs_size_bytes(inputs) 79 + compact_len(change.len() as u128) 80 + compact_outputs_size_bytes(change) 81 + compact_len(u128::from(*amount)) 82 + compact_len(u128::from(*fee)) 83 + anchor.as_ref().map_or(0, |anchor| hash_size_bytes(anchor)) 84 + signature_size_bytes(signature) 85 } 86 Transaction::Mine { 87 recipient, 88 anchor, 89 salt, 90 nonce, 91 difficulty_bits, 92 proof_header, 93 signature, 94 } => { 95 1 + address_size_bytes(recipient) 96 + hash_size_bytes(anchor) 97 + compact_len(u128::from(*salt)) 98 + compact_len(u128::from(*nonce)) 99 + compact_len(u128::from(*difficulty_bits)) 100 + 1 101 + proof_header 102 .as_ref() 103 .map(|header| stratum_header_size_bytes(header)) 104 .unwrap_or(0) 105 + hash_size_bytes(signature) 106 } 107 } 108 } 109 110 pub(crate) fn minimum_transfer_economic_size_bytes(input_count: usize) -> usize { 111 // A transfer always contains its tag, collection lengths, one output, fee, 112 // and signature. Each input contains at least a 32-byte transaction id, 113 // one byte for its output index, and a 32-byte owner address. 114 1 + compact_len(input_count as u128) 115 + input_count * (HASH_BYTES + 1 + PUBLIC_KEY_BYTES) 116 + compact_len(1) 117 + PUBLIC_KEY_BYTES 118 + 1 119 + 1 120 + SIGNATURE_BYTES 121 } 122 123 fn compact_inputs_size_bytes(inputs: &[TxInput]) -> usize { 124 inputs 125 .iter() 126 .map(|input| { 127 protocol_id_size_bytes(&input.outpoint.txid) 128 + compact_len(u128::from(input.outpoint.index)) 129 + address_size_bytes(&input.owner) 130 }) 131 .sum() 132 } 133 134 fn compact_outputs_size_bytes(outputs: &[TxOutput]) -> usize { 135 outputs.iter().map(compact_output_size_bytes).sum() 136 } 137 138 fn compact_output_size_bytes(output: &TxOutput) -> usize { 139 address_size_bytes(&output.address) + compact_len(u128::from(output.amount)) 140 } 141 142 fn address_size_bytes(address: &str) -> usize { 143 debug_assert!(validate_address(address, "debug address").is_ok()); 144 PUBLIC_KEY_BYTES 145 } 146 147 fn hash_size_bytes(hash: &str) -> usize { 148 debug_assert!(validate_hash(hash, "debug hash").is_ok()); 149 HASH_BYTES 150 } 151 152 fn signature_size_bytes(signature: &str) -> usize { 153 debug_assert!(validate_signature(signature, "debug signature").is_ok()); 154 SIGNATURE_BYTES 155 } 156 157 fn stratum_header_size_bytes(header: &str) -> usize { 158 debug_assert!(validate_stratum_header(header).is_ok()); 159 STRATUM_MINE_HEADER_BYTES 160 } 161 162 fn protocol_id_size_bytes(value: &str) -> usize { 163 match decode_hex(value).map(|bytes| bytes.len()) { 164 Ok(HASH_BYTES) => HASH_BYTES, 165 Ok(SIGNATURE_BYTES) => SIGNATURE_BYTES, 166 _ => SIGNATURE_BYTES, 167 } 168 } 169 170 pub(super) fn compact_len(mut value: u128) -> usize { 171 let mut bytes = 1; 172 while value >= 0x80 { 173 value >>= 7; 174 bytes += 1; 175 } 176 bytes 177 } 178 179 #[cfg(test)] 180 mod tests { 181 use super::{ 182 canonical_transaction_size_bytes, compact_len, minimum_transfer_economic_size_bytes, 183 validate_address, validate_hash, validate_protocol_id, validate_signature, 184 validate_stratum_header, 185 }; 186 use crate::domain::{OutPoint, Transaction, TxInput, TxOutput}; 187 188 #[test] 189 fn validators_accept_expected_protocol_lengths() { 190 assert!(validate_address(&"0".repeat(64), "test").is_ok()); 191 assert!(validate_hash(&"0".repeat(64), "test").is_ok()); 192 assert!(validate_signature(&"0".repeat(128), "test").is_ok()); 193 assert!(validate_stratum_header(&"0".repeat(160)).is_ok()); 194 assert!(validate_protocol_id(&"0".repeat(64), "test").is_ok()); 195 assert!(validate_protocol_id(&"0".repeat(128), "test").is_ok()); 196 } 197 198 #[test] 199 fn validators_reject_wrong_lengths_and_bad_hex() { 200 assert!(validate_address(&"0".repeat(62), "test").is_err()); 201 assert!(validate_hash(&"0".repeat(66), "test").is_err()); 202 assert!(validate_signature("zz", "test").is_err()); 203 assert!(validate_stratum_header(&"0".repeat(158)).is_err()); 204 assert!(validate_protocol_id(&"0".repeat(96), "test").is_err()); 205 } 206 207 #[test] 208 fn validators_keep_accepting_legacy_uppercase_hex() { 209 assert!(validate_address(&"AB".repeat(32), "test").is_ok()); 210 assert!(validate_hash(&"AB".repeat(32), "test").is_ok()); 211 assert!(validate_signature(&"AB".repeat(64), "test").is_ok()); 212 assert!(validate_stratum_header(&"AB".repeat(80)).is_ok()); 213 assert!(validate_protocol_id(&"AB".repeat(32), "test").is_ok()); 214 } 215 216 #[test] 217 fn compact_len_uses_base_128_varint_width() { 218 assert_eq!(compact_len(0), 1); 219 assert_eq!(compact_len(127), 1); 220 assert_eq!(compact_len(128), 2); 221 assert_eq!(compact_len(16_383), 2); 222 assert_eq!(compact_len(16_384), 3); 223 } 224 225 #[test] 226 fn transaction_economic_size_uses_binary_ids_and_compact_integers() { 227 let input = TxInput { 228 outpoint: OutPoint { 229 txid: "a".repeat(64), 230 index: 128, 231 }, 232 owner: "b".repeat(64), 233 signature: "c".repeat(128), 234 }; 235 let output = TxOutput { 236 address: "d".repeat(64), 237 amount: 128, 238 }; 239 let tx = Transaction::Transfer { 240 inputs: vec![input], 241 outputs: vec![output], 242 fee: 16_384, 243 signature: "e".repeat(128), 244 }; 245 246 assert_eq!( 247 canonical_transaction_size_bytes(&tx), 248 1 // transaction kind 249 + 1 // input count 250 + 32 // txid, counted as bytes rather than hex chars 251 + 2 // output index varint 252 + 32 // owner public key bytes 253 + 1 // output count 254 + 32 // recipient public key bytes 255 + 2 // amount varint 256 + 3 // fee varint 257 + 64 // signature bytes 258 ); 259 assert!(tx.serialized_size_bytes().unwrap() > canonical_transaction_size_bytes(&tx)); 260 assert_eq!(minimum_transfer_economic_size_bytes(1), 166); 261 assert!(minimum_transfer_economic_size_bytes(1) <= canonical_transaction_size_bytes(&tx)); 262 } 263 }