Skip to main content

host_encoding/
dotns.rs

1//! Pure DOTNS encoding and decoding functions.
2//!
3//! No I/O. No network calls. WASM-safe.
4//!
5//! Provides ENS-style namehashing, ABI encoding, SCALE encoding/decoding,
6//! and contenthash → CID conversion for the DOTNS resolver protocol.
7
8use tiny_keccak::{Hasher, Keccak};
9
10/// Solidity function selector for `contenthash(bytes32)`.
11pub const CONTENTHASH_SELECTOR: [u8; 4] = [0xbc, 0x1c, 0x58, 0xd1];
12
13/// Compile-time hex string to 20-byte address.
14pub const fn hex_addr(s: &str) -> [u8; 20] {
15    let b = s.as_bytes();
16    assert!(b.len() == 40, "address must be 40 hex chars");
17    let mut out = [0u8; 20];
18    let mut i = 0;
19    while i < 20 {
20        out[i] = (hex_nibble(b[i * 2]) << 4) | hex_nibble(b[i * 2 + 1]);
21        i += 1;
22    }
23    out
24}
25
26/// Compile-time hex character to nibble value.
27pub const fn hex_nibble(c: u8) -> u8 {
28    match c {
29        b'0'..=b'9' => c - b'0',
30        b'a'..=b'f' => c - b'a' + 10,
31        b'A'..=b'F' => c - b'A' + 10,
32        _ => panic!("invalid hex"),
33    }
34}
35
36/// Compute keccak256 hash of the given data.
37pub fn keccak256(data: &[u8]) -> [u8; 32] {
38    let mut hasher = Keccak::v256();
39    hasher.update(data);
40    let mut out = [0u8; 32];
41    hasher.finalize(&mut out);
42    out
43}
44
45/// ENS-style namehash: `namehash("mytestapp.dot")`.
46///
47/// Splits by `.`, reverses labels, and iteratively hashes to produce a
48/// 32-byte node identifier compatible with the ENS/DOTNS registry.
49pub fn namehash(domain: &str) -> [u8; 32] {
50    if domain.is_empty() {
51        return [0u8; 32];
52    }
53    let labels: Vec<&str> = domain.split('.').collect();
54    let mut node = [0u8; 32];
55    for label in labels.into_iter().rev() {
56        let label_hash = keccak256(label.as_bytes());
57        let mut buf = [0u8; 64];
58        buf[..32].copy_from_slice(&node);
59        buf[32..].copy_from_slice(&label_hash);
60        node = keccak256(&buf);
61    }
62    node
63}
64
65/// ABI-encode `contenthash(bytes32 node)` call data.
66pub fn encode_contenthash_call(node: &[u8; 32]) -> Vec<u8> {
67    let mut data = Vec::with_capacity(36);
68    data.extend_from_slice(&CONTENTHASH_SELECTOR);
69    data.extend_from_slice(node);
70    data
71}
72
73/// SCALE-encode the parameters for `ReviveApi::call()` runtime API.
74///
75/// Parameters (in order, from runtime metadata):
76///   origin: AccountId32 ([u8; 32])
77///   dest: H160 ([u8; 20])
78///   value: u128 (BalanceOf<T>, 16 bytes LE)
79///   gas_limit: Option<Weight> where Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
80///   storage_deposit_limit: Option<u128>
81///   input_data: Vec<u8>
82pub fn scale_encode_revive_call(dest: &[u8; 20], input_data: &[u8]) -> Result<Vec<u8>, String> {
83    let mut buf = Vec::with_capacity(128 + input_data.len());
84
85    // origin: AccountId32 (Alice — matches dot.li's dry-run convention)
86    buf.extend_from_slice(&[
87        0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f,
88        0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d,
89        0xa2, 0x7d,
90    ]);
91
92    // dest: H160
93    buf.extend_from_slice(dest);
94
95    // value: u128 = 0
96    buf.extend_from_slice(&0u128.to_le_bytes());
97
98    // gas_limit: Option<Weight> = Some(Weight { ref_time: MAX, proof_size: MAX })
99    buf.push(0x01); // Some
100    scale_compact_u64(&mut buf, u64::MAX); // ref_time
101    scale_compact_u64(&mut buf, u64::MAX); // proof_size
102
103    // storage_deposit_limit: Option<u128> = Some(u64::MAX as u128)
104    buf.push(0x01); // Some
105    buf.extend_from_slice(&(u64::MAX as u128).to_le_bytes());
106
107    // input_data: Vec<u8> = compact_len ++ bytes
108    scale_compact_len(&mut buf, input_data.len())?;
109    buf.extend_from_slice(input_data);
110
111    Ok(buf)
112}
113
114/// SCALE compact encoding for a u64 value.
115pub fn scale_compact_u64(buf: &mut Vec<u8>, val: u64) {
116    if val < 64 {
117        buf.push((val as u8) << 2);
118    } else if val < 0x4000 {
119        let v = ((val as u16) << 2) | 1;
120        buf.extend_from_slice(&v.to_le_bytes());
121    } else if val < 0x4000_0000 {
122        let v = ((val as u32) << 2) | 2;
123        buf.extend_from_slice(&v.to_le_bytes());
124    } else {
125        // Big integer mode: upper 6 bits = (byte_count - 4), lower 2 bits = 0b11
126        // For u64, we need up to 8 bytes
127        let bytes = val.to_le_bytes();
128        let len = 8 - (val.leading_zeros() / 8) as usize;
129        let len = len.max(4); // minimum 4 bytes in big mode
130        let prefix = (((len - 4) as u8) << 2) | 3;
131        buf.push(prefix);
132        buf.extend_from_slice(&bytes[..len]);
133    }
134}
135
136/// SCALE compact encoding for a length prefix.
137///
138/// Returns an error if `n` is >= 1 GiB, which is beyond the SCALE
139/// single-byte-mode range and not expected in any RPC call this crate makes.
140pub fn scale_compact_len(buf: &mut Vec<u8>, n: usize) -> Result<(), String> {
141    if n < 64 {
142        buf.push((n as u8) << 2);
143    } else if n < 16384 {
144        let v = ((n as u16) << 2) | 1;
145        buf.extend_from_slice(&v.to_le_bytes());
146    } else if n < 1_073_741_824 {
147        let v = ((n as u32) << 2) | 2;
148        buf.extend_from_slice(&v.to_le_bytes());
149    } else {
150        // Big mode — values >= 1 GiB are not valid for any RPC input_data this
151        // crate produces; return an error instead of panicking.
152        return Err(format!(
153            "compact encoding: value {n} is too large (max 1_073_741_823)"
154        ));
155    }
156    Ok(())
157}
158
159// Re-export shared hex utilities from the crate root.
160pub use crate::{hex_decode, hex_encode};
161
162/// Decode the SCALE-encoded `ContractResult` from pallet-revive's `ReviveApi::call`.
163///
164/// # Layout (pallet-revive, Asset Hub Paseo, 2025-03)
165///
166/// This is a hand-rolled positional decoder. The field order is:
167/// - gas_consumed: Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
168/// - gas_required: Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
169/// - storage_deposit: StorageDeposit enum (1 byte tag + u128)
170/// - [pallet-revive extra] Option<Balance> (1 tag + 16 if Some)
171/// - [pallet-revive extra] Balance (16 bytes u128)
172/// - debug_message: Vec<u8> (Compact len + bytes)
173/// - result: ExecReturnValue { flags: u32, data: Vec<u8> } (no Result wrapper)
174///
175/// If pallet-revive changes its ContractResult layout, this decoder must be
176/// updated and a new test vector captured.
177pub fn decode_contract_result(response: &[u8]) -> Result<Vec<u8>, String> {
178    let mut pos = 0;
179
180    // gas_consumed: Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
181    let (_, n) = decode_scale_compact(&response[pos..])?;
182    pos += n;
183    let (_, n) = decode_scale_compact(&response[pos..])?;
184    pos += n;
185
186    // gas_required: Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
187    let (_, n) = decode_scale_compact(&response[pos..])?;
188    pos += n;
189    let (_, n) = decode_scale_compact(&response[pos..])?;
190    pos += n;
191
192    // storage_deposit: StorageDeposit enum (1 byte variant + u128)
193    if pos + 17 > response.len() {
194        return Err("response too short (storage_deposit)".into());
195    }
196    pos += 1 + 16;
197
198    // pallet-revive adds extra fields not present in pallet-contracts:
199    // - Option<Balance> (1 tag + 16 u128 if Some) — likely storage deposit limit
200    // - Balance (16 bytes u128) — likely eth gas price or fee
201    if pos >= response.len() {
202        return Err("response too short (extra fields)".into());
203    }
204    let opt_tag = response[pos];
205    pos += 1; // Option tag
206    if opt_tag == 1 {
207        if pos + 16 > response.len() {
208            return Err("response too short (option balance)".into());
209        }
210        pos += 16; // Some(u128)
211    }
212    if pos + 16 > response.len() {
213        return Err("response too short (plain balance)".into());
214    }
215    pos += 16; // plain u128
216
217    // debug_message: Vec<u8>
218    let (msg_len, bytes_read) = decode_scale_compact(&response[pos..])?;
219    pos += bytes_read + msg_len;
220
221    // result: ExecReturnValue { flags: u32, data: Vec<u8> } (no Result wrapper in pallet-revive)
222    if pos + 4 > response.len() {
223        return Err("response too short (flags)".into());
224    }
225    pos += 4; // flags: u32
226
227    // data: Vec<u8>
228    let (data_len, bytes_read) = decode_scale_compact(&response[pos..])?;
229    pos += bytes_read;
230
231    if pos + data_len > response.len() {
232        return Err(format!(
233            "data extends beyond response (pos={pos}, data_len={data_len}, total={})",
234            response.len()
235        ));
236    }
237
238    Ok(response[pos..pos + data_len].to_vec())
239}
240
241/// Decode a SCALE compact-encoded integer, returning (value, bytes_consumed).
242pub fn decode_scale_compact(data: &[u8]) -> Result<(usize, usize), String> {
243    if data.is_empty() {
244        return Err("empty data for compact decode".into());
245    }
246    let mode = data[0] & 0b11;
247    match mode {
248        0 => Ok(((data[0] >> 2) as usize, 1)),
249        1 => {
250            if data.len() < 2 {
251                return Err("compact: need 2 bytes".into());
252            }
253            let v = u16::from_le_bytes([data[0], data[1]]) >> 2;
254            Ok((v as usize, 2))
255        }
256        2 => {
257            if data.len() < 4 {
258                return Err("compact: need 4 bytes".into());
259            }
260            let v = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) >> 2;
261            Ok((v as usize, 4))
262        }
263        3 => {
264            // Big integer mode
265            let bytes_needed = (data[0] >> 2) as usize + 4;
266            if data.len() < 1 + bytes_needed {
267                return Err("compact: big mode insufficient data".into());
268            }
269            let mut val: usize = 0;
270            for i in (0..bytes_needed).rev() {
271                val = (val << 8) | data[1 + i] as usize;
272            }
273            Ok((val, 1 + bytes_needed))
274        }
275        _ => unreachable!(),
276    }
277}
278
279/// Decode ABI-encoded bytes return value from Solidity.
280///
281/// The `contenthash` function returns `bytes` which is ABI-encoded as:
282///   offset (32 bytes, = 0x20)
283///   length (32 bytes)
284///   data (padded to 32-byte boundary)
285pub fn decode_abi_bytes(data: &[u8]) -> Result<Vec<u8>, String> {
286    if data.len() < 64 {
287        return Err(format!("ABI bytes too short: {} bytes", data.len()));
288    }
289    // First 32 bytes: offset (should be 0x20 = 32)
290    // Next 32 bytes: length
291    let len = u32::from_be_bytes([data[60], data[61], data[62], data[63]]) as usize;
292    if 64 + len > data.len() {
293        return Err(format!("ABI bytes: length {len} exceeds data"));
294    }
295    Ok(data[64..64 + len].to_vec())
296}
297
298/// Parse a contenthash value to extract an IPFS CIDv1.
299///
300/// The contenthash format follows EIP-1577:
301///   0xe3 0x01 0x01 <multihash>  (IPFS, codec dag-pb, CIDv1)
302///   0xe5 0x01 ...               (Swarm)
303///
304/// We decode the CID and return it as a base32-encoded string (multibase prefix `b`).
305pub fn contenthash_to_cid(data: &[u8]) -> Result<String, String> {
306    if data.is_empty() {
307        return Err("empty contenthash".into());
308    }
309
310    // EIP-1577 contenthash uses multicodec varint prefix.
311    // IPFS namespace = 0xe3 = 227, encoded as varint `e3 01` (2 bytes).
312    // Swarm namespace = 0xe5 = 229, encoded as varint `e5 01` (2 bytes).
313    let (codec, varint_len) = decode_unsigned_varint(data);
314    match codec {
315        0xe3 => {
316            // IPFS — skip the namespace varint, rest is the CID
317            let cid_bytes = &data[varint_len..];
318            Ok(format!("b{}", base32_encode(cid_bytes)))
319        }
320        0xe5 => Err("Swarm contenthash not supported".into()),
321        _ => Err(format!(
322            "contenthash_to_cid: unrecognized codec varint 0x{codec:02x}; only IPFS dag-pb (0x70) is supported"
323        )),
324    }
325}
326
327/// Decode an unsigned varint (LEB128), returning (value, bytes_consumed).
328pub fn decode_unsigned_varint(data: &[u8]) -> (u64, usize) {
329    let mut value: u64 = 0;
330    let mut shift = 0;
331    for (i, &byte) in data.iter().enumerate() {
332        if shift >= 64 {
333            break;
334        }
335        value |= ((byte & 0x7f) as u64) << shift;
336        if byte & 0x80 == 0 {
337            return (value, i + 1);
338        }
339        shift += 7;
340    }
341    (value, data.len())
342}
343
344/// RFC 4648 base32 encoding (lowercase, no padding).
345pub fn base32_encode(data: &[u8]) -> String {
346    const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
347    let mut result = String::new();
348    let mut bits: u32 = 0;
349    let mut num_bits: u32 = 0;
350    for &byte in data {
351        bits = (bits << 8) | byte as u32;
352        num_bits += 8;
353        while num_bits >= 5 {
354            num_bits -= 5;
355            result.push(ALPHABET[((bits >> num_bits) & 0x1f) as usize] as char);
356        }
357    }
358    if num_bits > 0 {
359        result.push(ALPHABET[((bits << (5 - num_bits)) & 0x1f) as usize] as char);
360    }
361    result
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_hex_nibble_digits() {
370        assert_eq!(hex_nibble(b'0'), 0);
371        assert_eq!(hex_nibble(b'9'), 9);
372        assert_eq!(hex_nibble(b'a'), 10);
373        assert_eq!(hex_nibble(b'f'), 15);
374        assert_eq!(hex_nibble(b'A'), 10);
375        assert_eq!(hex_nibble(b'F'), 15);
376    }
377
378    #[test]
379    fn test_hex_addr_roundtrip() {
380        let addr = hex_addr("7756DF72CBc7f062e7403cD59e45fBc78bed1cD7");
381        assert_eq!(addr[0], 0x77);
382        assert_eq!(addr[19], 0xd7);
383    }
384
385    #[test]
386    fn test_keccak256_empty() {
387        // keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
388        let result = keccak256(b"");
389        assert_eq!(result[0], 0xc5);
390        assert_eq!(result[1], 0xd2);
391    }
392
393    #[test]
394    fn test_namehash_empty_domain() {
395        assert_eq!(namehash(""), [0u8; 32]);
396    }
397
398    #[test]
399    fn test_encode_contenthash_call_length() {
400        let node = [0u8; 32];
401        let call = encode_contenthash_call(&node);
402        // 4 bytes selector + 32 bytes node
403        assert_eq!(call.len(), 36);
404        assert_eq!(&call[..4], &CONTENTHASH_SELECTOR);
405    }
406
407    #[test]
408    fn test_scale_compact_u64_single_byte() {
409        let mut buf = Vec::new();
410        scale_compact_u64(&mut buf, 0);
411        assert_eq!(buf, vec![0x00]);
412
413        buf.clear();
414        scale_compact_u64(&mut buf, 63);
415        assert_eq!(buf, vec![0xfc]);
416    }
417
418    #[test]
419    fn test_scale_compact_len_too_large_returns_error() {
420        let mut buf = Vec::new();
421        let result = scale_compact_len(&mut buf, 1_073_741_824);
422        assert!(result.is_err());
423    }
424
425    #[test]
426    fn test_hex_encode_decode_roundtrip() {
427        let original = vec![0xde, 0xad, 0xbe, 0xef];
428        let encoded = hex_encode(&original);
429        assert_eq!(encoded, "0xdeadbeef");
430        let decoded = hex_decode(&encoded).unwrap();
431        assert_eq!(decoded, original);
432    }
433
434    #[test]
435    fn test_hex_decode_rejects_odd_length() {
436        assert!(hex_decode("0xabc").is_none());
437    }
438
439    #[test]
440    fn test_decode_scale_compact_single_byte() {
441        let (val, consumed) = decode_scale_compact(&[0x04]).unwrap();
442        assert_eq!(val, 1);
443        assert_eq!(consumed, 1);
444    }
445
446    #[test]
447    fn test_decode_scale_compact_empty_returns_error() {
448        assert!(decode_scale_compact(&[]).is_err());
449    }
450
451    #[test]
452    fn test_decode_abi_bytes_too_short_returns_error() {
453        assert!(decode_abi_bytes(&[0u8; 32]).is_err());
454    }
455
456    #[test]
457    fn test_contenthash_to_cid_empty_returns_error() {
458        assert!(contenthash_to_cid(&[]).is_err());
459    }
460
461    #[test]
462    fn test_contenthash_to_cid_swarm_returns_error() {
463        // 0xe5 0x01 is the Swarm namespace varint
464        assert!(contenthash_to_cid(&[0xe5, 0x01, 0x00]).is_err());
465    }
466
467    #[test]
468    fn test_contenthash_to_cid_unknown_codec_returns_error() {
469        // 0x01 is not a recognized namespace
470        let result = contenthash_to_cid(&[0x01, 0x02, 0x03]);
471        assert!(result.is_err());
472        let msg = result.unwrap_err();
473        assert!(msg.contains("unrecognized codec varint"));
474    }
475
476    #[test]
477    fn test_base32_encode_known_vector() {
478        // base32("") = ""
479        assert_eq!(base32_encode(b""), "");
480        // base32("f") = "my" (RFC 4648, lowercase)
481        assert_eq!(base32_encode(b"f"), "my");
482    }
483
484    #[test]
485    fn test_decode_unsigned_varint_single_byte() {
486        let (val, consumed) = decode_unsigned_varint(&[0x05]);
487        assert_eq!(val, 5);
488        assert_eq!(consumed, 1);
489    }
490
491    #[test]
492    fn test_decode_unsigned_varint_multi_byte() {
493        // LEB128 encoding of 300 = 0xAC 0x02
494        let (val, consumed) = decode_unsigned_varint(&[0xac, 0x02]);
495        assert_eq!(val, 300);
496        assert_eq!(consumed, 2);
497    }
498
499    /// Pinned vector: namehash("mytestapp.dot") must match the on-chain value
500    /// used by dot.li and verified in host-chain's test_encoding_matches_dotli.
501    #[test]
502    fn test_namehash_mytestapp_dot_pinned() {
503        let node = namehash("mytestapp.dot");
504        // Verified against host-chain::dotns::test_encoding_matches_dotli
505        // (bytes 142..174 of the encoded params = the namehash in the ABI calldata).
506        let hex = hex_encode(&node);
507        // The namehash appears in the known encoded params at the contenthash(bytes32) position.
508        // Just verify it's deterministic and non-zero.
509        assert_ne!(node, [0u8; 32]);
510        // Pinned value from the first successful run:
511        let expected = hex_decode(&hex).unwrap();
512        assert_eq!(node.to_vec(), expected);
513        // Cross-check: the encoding test in host-chain pins the full params string;
514        // here we just verify the namehash portion is stable.
515        assert_eq!(
516            hex,
517            "0xea3cb49a7f22581a2b768fdfd30be01a398514934d65b60e158ee9ee93c20894"
518        );
519    }
520
521    /// Pinned vector: full SCALE-encoded ReviveApi::call() params for
522    /// mytestapp.dot contenthash query, matching dot.li's state_call.
523    #[test]
524    fn test_scale_encode_revive_call_pinned() {
525        let content_resolver: [u8; 20] = hex_addr("7756DF72CBc7f062e7403cD59e45fBc78bed1cD7");
526        let node = namehash("mytestapp.dot");
527        let call_data = encode_contenthash_call(&node);
528        let params = scale_encode_revive_call(&content_resolver, &call_data).unwrap();
529        let expected = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d7756df72cbc7f062e7403cd59e45fbc78bed1cd7000000000000000000000000000000000113ffffffffffffffff13ffffffffffffffff01ffffffffffffffff000000000000000090bc1c58d1ea3cb49a7f22581a2b768fdfd30be01a398514934d65b60e158ee9ee93c20894";
530        assert_eq!(hex_encode(&params), expected);
531    }
532
533    /// Pinned vector: contenthash → CIDv1 base32 for a known IPFS hash.
534    #[test]
535    fn test_contenthash_to_cid_ipfs_pinned() {
536        // IPFS namespace 0xe3 0x01, followed by CIDv1 (0x01), dag-pb (0x70),
537        // sha2-256 (0x12), length 32, then the 32-byte digest.
538        // This is the contenthash for bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
539        let digest =
540            hex_decode("0xc3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a")
541                .unwrap();
542        let mut contenthash = vec![0xe3, 0x01, 0x01, 0x70, 0x12, 0x20];
543        contenthash.extend_from_slice(&digest);
544        let cid = contenthash_to_cid(&contenthash).unwrap();
545        assert!(cid.starts_with("bafybei"));
546    }
547
548    // -----------------------------------------------------------------------
549    // scale_compact_u64 — multi-mode coverage
550    // -----------------------------------------------------------------------
551
552    #[test]
553    fn test_scale_compact_u64_two_byte_mode() {
554        // 64 <= val < 0x4000 → two-byte mode, lower 2 bits = 0b01
555        let mut buf = Vec::new();
556        scale_compact_u64(&mut buf, 64);
557        // 64 << 2 | 1 = 257 = 0x0101 LE
558        assert_eq!(buf, vec![0x01, 0x01]);
559    }
560
561    #[test]
562    fn test_scale_compact_u64_four_byte_mode() {
563        // 0x4000 <= val < 0x4000_0000 → four-byte mode, lower 2 bits = 0b10
564        let mut buf = Vec::new();
565        scale_compact_u64(&mut buf, 0x4000);
566        // 0x4000 << 2 | 2 = 0x10002 LE 4-byte
567        let expected_val: u32 = (0x4000u32 << 2) | 2;
568        assert_eq!(buf, expected_val.to_le_bytes());
569    }
570
571    #[test]
572    fn test_scale_compact_u64_big_mode_u64_max() {
573        // u64::MAX requires big-integer mode
574        let mut buf = Vec::new();
575        scale_compact_u64(&mut buf, u64::MAX);
576        // prefix byte: mode=3, byte_count=8, so upper 6 bits = (8-4)=4, lower 2 = 0b11
577        // prefix = (4 << 2) | 3 = 19 = 0x13
578        assert_eq!(buf[0], 0x13);
579        // The 8 LE bytes of u64::MAX follow
580        assert_eq!(buf.len(), 9);
581        assert_eq!(&buf[1..], &u64::MAX.to_le_bytes());
582    }
583
584    // -----------------------------------------------------------------------
585    // scale_compact_len — multi-mode coverage
586    // -----------------------------------------------------------------------
587
588    #[test]
589    fn test_scale_compact_len_single_byte_boundary() {
590        let mut buf = Vec::new();
591        scale_compact_len(&mut buf, 0).unwrap();
592        assert_eq!(buf, vec![0x00]);
593
594        buf.clear();
595        scale_compact_len(&mut buf, 63).unwrap();
596        assert_eq!(buf, vec![0xfc]); // 63 << 2 = 252
597    }
598
599    #[test]
600    fn test_scale_compact_len_two_byte_mode() {
601        let mut buf = Vec::new();
602        scale_compact_len(&mut buf, 64).unwrap();
603        // 64 << 2 | 1 = 257 = 0x0101 LE
604        assert_eq!(buf, vec![0x01, 0x01]);
605    }
606
607    #[test]
608    fn test_scale_compact_len_four_byte_mode() {
609        let mut buf = Vec::new();
610        scale_compact_len(&mut buf, 16384).unwrap();
611        // 16384 << 2 | 2 LE 4-byte
612        let expected: u32 = (16384u32 << 2) | 2;
613        assert_eq!(buf, expected.to_le_bytes());
614    }
615
616    // -----------------------------------------------------------------------
617    // decode_scale_compact — multi-mode and truncation errors
618    // -----------------------------------------------------------------------
619
620    #[test]
621    fn test_decode_scale_compact_two_byte_mode() {
622        // Encode 64 as two-byte compact: 64 << 2 | 1 = 257 = [0x01, 0x01]
623        let (val, consumed) = decode_scale_compact(&[0x01, 0x01]).unwrap();
624        assert_eq!(val, 64);
625        assert_eq!(consumed, 2);
626    }
627
628    #[test]
629    fn test_decode_scale_compact_two_byte_truncated_returns_error() {
630        // Mode bit = 0b01 but only one byte provided
631        assert!(decode_scale_compact(&[0x01]).is_err());
632    }
633
634    #[test]
635    fn test_decode_scale_compact_four_byte_mode() {
636        // Encode 16384 as four-byte compact: 16384 << 2 | 2 LE
637        let v: u32 = (16384u32 << 2) | 2;
638        let input = v.to_le_bytes();
639        let (val, consumed) = decode_scale_compact(&input).unwrap();
640        assert_eq!(val, 16384);
641        assert_eq!(consumed, 4);
642    }
643
644    #[test]
645    fn test_decode_scale_compact_four_byte_truncated_returns_error() {
646        // Mode bit = 0b10 but fewer than 4 bytes
647        assert!(decode_scale_compact(&[0x02, 0x00, 0x00]).is_err());
648    }
649
650    #[test]
651    fn test_decode_scale_compact_big_mode() {
652        // Big mode: prefix = 0x03 means (0 extra bytes beyond 4) → need 4 bytes
653        // Let's encode value 0x01020304 in big mode:
654        // prefix byte with byte_count=4: upper 6 = (4-4)=0, lower 2 = 3 → prefix=0x03
655        let input = [0x03u8, 0x04, 0x03, 0x02, 0x01];
656        let (val, consumed) = decode_scale_compact(&input).unwrap();
657        assert_eq!(val, 0x01020304);
658        assert_eq!(consumed, 5); // 1 prefix + 4 data bytes
659    }
660
661    #[test]
662    fn test_decode_scale_compact_big_mode_insufficient_data_returns_error() {
663        // prefix byte claims 4 bytes needed but we only provide 3
664        let input = [0x03u8, 0x04, 0x03, 0x02];
665        assert!(decode_scale_compact(&input).is_err());
666    }
667
668    // -----------------------------------------------------------------------
669    // decode_abi_bytes — success path
670    // -----------------------------------------------------------------------
671
672    #[test]
673    fn test_decode_abi_bytes_valid() {
674        // ABI encoding of bytes: offset=0x20 (32 bytes), length=5, data="hello"
675        let mut data = vec![0u8; 64];
676        // First 32 bytes = offset = 0x20 (written BE)
677        data[31] = 0x20;
678        // Next 32 bytes = length = 5 (written BE)
679        data[63] = 5;
680        data.extend_from_slice(b"hello");
681        let result = decode_abi_bytes(&data).unwrap();
682        assert_eq!(result, b"hello");
683    }
684
685    #[test]
686    fn test_decode_abi_bytes_zero_length() {
687        // ABI encoding of empty bytes: offset=0x20, length=0, no data
688        let mut data = vec![0u8; 64];
689        data[31] = 0x20;
690        // length = 0 — nothing after the header
691        let result = decode_abi_bytes(&data).unwrap();
692        assert_eq!(result, b"");
693    }
694
695    #[test]
696    fn test_decode_abi_bytes_length_exceeds_data_returns_error() {
697        // Claims length=100 but only 64 bytes total
698        let mut data = vec![0u8; 64];
699        data[31] = 0x20;
700        data[63] = 100; // length claims 100 bytes but nothing follows
701        assert!(decode_abi_bytes(&data).is_err());
702    }
703
704    // -----------------------------------------------------------------------
705    // hex_decode — invalid hex character path
706    // -----------------------------------------------------------------------
707
708    #[test]
709    fn test_hex_decode_rejects_invalid_chars() {
710        assert!(hex_decode("0xgg").is_none());
711        assert!(hex_decode("zz").is_none());
712    }
713
714    // -----------------------------------------------------------------------
715    // namehash — single-label domain
716    // -----------------------------------------------------------------------
717
718    #[test]
719    fn test_namehash_single_label() {
720        // "dot" is a single label with no separator; namehash should be non-zero
721        let node = namehash("dot");
722        assert_ne!(node, [0u8; 32]);
723        // Verify determinism
724        assert_eq!(node, namehash("dot"));
725    }
726
727    // -----------------------------------------------------------------------
728    // decode_contract_result — comprehensive coverage
729    // -----------------------------------------------------------------------
730
731    /// Build a minimal valid ContractResult buffer in pallet-revive format
732    /// with gas_consumed=0, gas_required=0, storage_deposit=Refund(0),
733    /// Option<Balance>=None, Balance=0, debug_message="", return_data=payload.
734    fn build_contract_result(payload: &[u8]) -> Vec<u8> {
735        let mut buf = Vec::new();
736
737        // gas_consumed: Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
738        scale_compact_u64(&mut buf, 0); // ref_time = 0 → single byte 0x00
739        scale_compact_u64(&mut buf, 0); // proof_size = 0
740
741        // gas_required: Weight
742        scale_compact_u64(&mut buf, 0);
743        scale_compact_u64(&mut buf, 0);
744
745        // storage_deposit: StorageDeposit (variant 0 = Charge, + 16 bytes u128)
746        buf.push(0x00); // variant
747        buf.extend_from_slice(&0u128.to_le_bytes()); // 16 bytes
748
749        // pallet-revive extra: Option<Balance> = None
750        buf.push(0x00); // None
751
752        // pallet-revive extra: Balance (u128) = 0
753        buf.extend_from_slice(&0u128.to_le_bytes()); // 16 bytes
754
755        // debug_message: Vec<u8> (compact len=0)
756        scale_compact_len(&mut buf, 0).unwrap();
757
758        // result: ExecReturnValue { flags: u32, data: Vec<u8> }
759        buf.extend_from_slice(&0u32.to_le_bytes()); // flags = 0
760        scale_compact_len(&mut buf, payload.len()).unwrap();
761        buf.extend_from_slice(payload);
762
763        buf
764    }
765
766    #[test]
767    fn test_decode_contract_result_empty_payload() {
768        let buf = build_contract_result(b"");
769        let result = decode_contract_result(&buf).unwrap();
770        assert_eq!(result, b"");
771    }
772
773    #[test]
774    fn test_decode_contract_result_with_payload() {
775        let payload = b"\x01\x02\x03\x04";
776        let buf = build_contract_result(payload);
777        let result = decode_contract_result(&buf).unwrap();
778        assert_eq!(result, payload);
779    }
780
781    #[test]
782    fn test_decode_contract_result_with_option_some() {
783        // Build a result where the Option<Balance> = Some(value)
784        let mut buf = Vec::new();
785        scale_compact_u64(&mut buf, 0); // gas_consumed ref_time
786        scale_compact_u64(&mut buf, 0); // gas_consumed proof_size
787        scale_compact_u64(&mut buf, 0); // gas_required ref_time
788        scale_compact_u64(&mut buf, 0); // gas_required proof_size
789        buf.push(0x00); // storage_deposit variant
790        buf.extend_from_slice(&0u128.to_le_bytes()); // storage_deposit value
791        buf.push(0x01); // Option<Balance> = Some
792        buf.extend_from_slice(&42u128.to_le_bytes()); // the u128 value
793        buf.extend_from_slice(&0u128.to_le_bytes()); // extra Balance
794        scale_compact_len(&mut buf, 0).unwrap(); // debug_message empty
795        buf.extend_from_slice(&0u32.to_le_bytes()); // flags
796        scale_compact_len(&mut buf, 3).unwrap(); // data len = 3
797        buf.extend_from_slice(b"abc");
798
799        let result = decode_contract_result(&buf).unwrap();
800        assert_eq!(result, b"abc");
801    }
802
803    #[test]
804    fn test_decode_contract_result_truncated_at_storage_deposit_returns_error() {
805        // Buffer with compacts for gas but too short for storage_deposit (17 bytes needed)
806        let mut buf = Vec::new();
807        scale_compact_u64(&mut buf, 0);
808        scale_compact_u64(&mut buf, 0);
809        scale_compact_u64(&mut buf, 0);
810        scale_compact_u64(&mut buf, 0);
811        // Only 5 bytes instead of 17 for storage_deposit
812        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00]);
813        assert!(decode_contract_result(&buf).is_err());
814    }
815
816    #[test]
817    fn test_decode_contract_result_truncated_plain_balance_returns_error() {
818        // Valid through storage_deposit and Option tag = None, but the plain Balance
819        // (16 bytes) is truncated to 4 bytes.
820        let mut buf = Vec::new();
821        scale_compact_u64(&mut buf, 0);
822        scale_compact_u64(&mut buf, 0);
823        scale_compact_u64(&mut buf, 0);
824        scale_compact_u64(&mut buf, 0);
825        buf.push(0x00); // storage_deposit variant
826        buf.extend_from_slice(&0u128.to_le_bytes()); // 16 bytes
827        buf.push(0x00); // Option = None
828        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // only 4 of 16 Balance bytes
829        assert!(decode_contract_result(&buf).is_err());
830    }
831
832    #[test]
833    fn test_decode_contract_result_truncated_option_some_balance_returns_error() {
834        // Option = Some but only 4 bytes for the u128 value (needs 16)
835        let mut buf = Vec::new();
836        scale_compact_u64(&mut buf, 0);
837        scale_compact_u64(&mut buf, 0);
838        scale_compact_u64(&mut buf, 0);
839        scale_compact_u64(&mut buf, 0);
840        buf.push(0x00); // storage_deposit variant
841        buf.extend_from_slice(&0u128.to_le_bytes()); // 16 bytes
842        buf.push(0x01); // Option = Some
843        buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // only 4 of 16 bytes
844        assert!(decode_contract_result(&buf).is_err());
845    }
846
847    #[test]
848    fn test_decode_contract_result_truncated_at_flags_returns_error() {
849        // Valid through debug_message but missing flags (4 bytes)
850        let mut buf = build_contract_result(b"payload");
851        // Truncate to just before flags: remove last (4 + compact_len(7) + 7) bytes
852        // build_contract_result appends: flags(4) + compact_len(7)(1) + payload(7) = 12
853        let len = buf.len();
854        buf.truncate(len - 12);
855        // Add just the debug_message (len=0 compact = 1 byte, already included in build_contract_result)
856        // but not flags
857        assert!(decode_contract_result(&buf).is_err());
858    }
859
860    #[test]
861    fn test_decode_contract_result_data_extends_beyond_response_returns_error() {
862        // Build a result that claims a data length larger than remaining bytes
863        let mut buf = build_contract_result(b"");
864        // The last bytes are: flags(4) + compact_len(0)(1) = 5 bytes
865        // Overwrite the compact_len byte to claim 100 bytes of data but provide none
866        let len = buf.len();
867        buf[len - 1] = 100u8 << 2; // compact-encode 100 in single-byte mode
868        assert!(decode_contract_result(&buf).is_err());
869    }
870
871    #[test]
872    fn test_decode_contract_result_with_debug_message() {
873        let mut buf = Vec::new();
874        scale_compact_u64(&mut buf, 0);
875        scale_compact_u64(&mut buf, 0);
876        scale_compact_u64(&mut buf, 0);
877        scale_compact_u64(&mut buf, 0);
878        buf.push(0x00);
879        buf.extend_from_slice(&0u128.to_le_bytes());
880        buf.push(0x00); // Option = None
881        buf.extend_from_slice(&0u128.to_le_bytes());
882        // debug_message = "debug"
883        scale_compact_len(&mut buf, 5).unwrap();
884        buf.extend_from_slice(b"debug");
885        buf.extend_from_slice(&0u32.to_le_bytes()); // flags
886        scale_compact_len(&mut buf, 4).unwrap();
887        buf.extend_from_slice(b"data");
888
889        let result = decode_contract_result(&buf).unwrap();
890        assert_eq!(result, b"data");
891    }
892
893    // -----------------------------------------------------------------------
894    // scale_encode_revive_call — error path for oversized input_data
895    // -----------------------------------------------------------------------
896
897    #[test]
898    fn test_scale_encode_revive_call_rejects_oversized_input() {
899        // input_data.len() >= 1 GiB triggers the scale_compact_len error.
900        // We can't actually allocate 1 GiB in a test, so we test by calling
901        // scale_compact_len directly (it's the internal gating function).
902        let mut buf = Vec::new();
903        let result = scale_compact_len(&mut buf, 1_073_741_824);
904        assert!(result.is_err());
905        let msg = result.unwrap_err();
906        assert!(msg.contains("too large"));
907    }
908}