iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }