Skip to main content

foundry_evm_traces/decoder/
precompiles.rs

1use crate::{CallTrace, DecodedCallData};
2use alloy_primitives::{Address, B256, U256, hex};
3use alloy_sol_types::{SolCall, abi, sol};
4use foundry_config::{Chain, NamedChain};
5use foundry_evm_core::{
6    precompiles::{
7        BLAKE_2F, BLS12_G1ADD, BLS12_G1MSM, BLS12_G2ADD, BLS12_G2MSM, BLS12_MAP_FP_TO_G1,
8        BLS12_MAP_FP2_TO_G2, BLS12_PAIRING_CHECK, CELO_TRANSFER, EC_ADD, EC_MUL, EC_PAIRING,
9        EC_RECOVER, IDENTITY, MOD_EXP, P256_VERIFY, POINT_EVALUATION, RIPEMD_160, SHA_256,
10    },
11    tempo::{TEMPO_PRECOMPILE_ADDRESSES, TEMPO_TIP20_TOKENS, active_tempo_precompile_addresses},
12};
13use foundry_evm_hardforks::TempoHardfork;
14use itertools::Itertools;
15use revm_inspectors::tracing::types::DecodedCallTrace;
16
17sol! {
18/// EVM precompiles interface. For illustration purposes only, as precompiles don't follow the
19/// Solidity ABI codec.
20///
21/// Parameter names and types are taken from [evm.codes](https://www.evm.codes/precompiled).
22interface Precompiles {
23    struct EcPairingInput {
24        uint256 x1;
25        uint256 y1;
26        uint256 x2;
27        uint256 y2;
28        uint256 x3;
29        uint256 y3;
30    }
31
32    /* 0x01 */ function ecrecover(bytes32 hash, uint8 v, uint256 r, uint256 s) returns (address publicAddress);
33    /* 0x02 */ function sha256(bytes data) returns (bytes32 hash);
34    /* 0x03 */ function ripemd(bytes data) returns (bytes20 hash);
35    /* 0x04 */ function identity(bytes data) returns (bytes data);
36    /* 0x05 */ function modexp(uint256 Bsize, uint256 Esize, uint256 Msize, bytes B, bytes E, bytes M) returns (bytes value);
37    /* 0x06 */ function ecadd(uint256 x1, uint256 y1, uint256 x2, uint256 y2) returns (uint256 x, uint256 y);
38    /* 0x07 */ function ecmul(uint256 x1, uint256 y1, uint256 s) returns (uint256 x, uint256 y);
39    /* 0x08 */ function ecpairing(EcPairingInput[] input) returns (bool success);
40    /* 0x09 */ function blake2f(uint32 rounds, uint64[8] h, uint64[16] m, uint64[2] t, bool f) returns (uint64[8] h);
41    /* 0x0a */ function pointEvaluation(bytes32 versionedHash, bytes32 z, bytes32 y, bytes1[48] commitment, bytes1[48] proof) returns (bytes value);
42
43    // Prague BLS12-381 precompiles (EIP-2537)
44    /* 0x0b */ function bls12G1Add(bytes p1, bytes p2) returns (bytes result);
45    /* 0x0c */ function bls12G1Msm(bytes[] scalarsAndPoints) returns (bytes result);
46    /* 0x0d */ function bls12G2Add(bytes p1, bytes p2) returns (bytes result);
47    /* 0x0e */ function bls12G2Msm(bytes[] scalarsAndPoints) returns (bytes result);
48    /* 0x0f */ function bls12PairingCheck(bytes[] pairs) returns (bool success);
49    /* 0x10 */ function bls12MapFpToG1(bytes fp) returns (bytes result);
50    /* 0x11 */ function bls12MapFp2ToG2(bytes fp2) returns (bytes result);
51
52    // Osaka precompiles (EIP-7212)
53    /* 0x100 */ function p256Verify(bytes32 hash, uint256 r, uint256 s, uint256 qx, uint256 qy) returns (bool success);
54}
55}
56use Precompiles::*;
57
58pub(super) fn is_known_precompile(
59    address: Address,
60    chain_id: Option<u64>,
61    tempo_hardfork: Option<TempoHardfork>,
62) -> bool {
63    // Standard EVM precompiles (all chains).
64    let is_standard = address[..19].iter().all(|&x| x == 0)
65        && matches!(
66            address,
67            EC_RECOVER
68                | SHA_256
69                | RIPEMD_160
70                | IDENTITY
71                | MOD_EXP
72                | EC_ADD
73                | EC_MUL
74                | EC_PAIRING
75                | BLAKE_2F
76                | POINT_EVALUATION
77                | BLS12_G1ADD
78                | BLS12_G1MSM
79                | BLS12_G2ADD
80                | BLS12_G2MSM
81                | BLS12_PAIRING_CHECK
82                | BLS12_MAP_FP_TO_G1
83                | BLS12_MAP_FP2_TO_G2
84                | P256_VERIFY
85        );
86    if is_standard {
87        return true;
88    }
89    // Tempo precompiles and TIP20 fee tokens (only on Tempo chains).
90    let is_tempo_precompile = match tempo_hardfork {
91        Some(hardfork) => active_tempo_precompile_addresses(hardfork).any(|addr| addr == address),
92        None => TEMPO_PRECOMPILE_ADDRESSES.contains(&address),
93    };
94    if chain_id.is_some_and(|id| Chain::from_id(id).is_tempo())
95        && (is_tempo_precompile || TEMPO_TIP20_TOKENS.contains(&address))
96    {
97        return true;
98    }
99    // Celo transfer precompile (only on Celo chains).
100    if chain_id.is_some_and(|id| {
101        matches!(Chain::from_id(id).named(), Some(NamedChain::Celo | NamedChain::CeloSepolia))
102    }) && address == CELO_TRANSFER
103    {
104        return true;
105    }
106    false
107}
108
109/// Tries to decode a precompile call. Returns `Some` if successful.
110pub(super) fn decode(
111    trace: &CallTrace,
112    chain_id: Option<u64>,
113    tempo_hardfork: Option<TempoHardfork>,
114) -> Option<DecodedCallTrace> {
115    if !is_known_precompile(trace.address, chain_id, tempo_hardfork) {
116        return None;
117    }
118
119    for &precompile in PRECOMPILES {
120        if trace.address == precompile.address() {
121            let signature = precompile.signature(&trace.data);
122
123            let args = precompile
124                .decode_call(&trace.data)
125                .unwrap_or_else(|_| vec![trace.data.to_string()]);
126
127            let return_data = precompile
128                .decode_return(&trace.output)
129                .unwrap_or_else(|_| vec![trace.output.to_string()]);
130            let return_data = if return_data.len() == 1 {
131                return_data.into_iter().next().unwrap()
132            } else {
133                format!("({})", return_data.join(", "))
134            };
135
136            return Some(DecodedCallTrace {
137                label: Some("PRECOMPILES".to_string()),
138                call_data: Some(DecodedCallData { signature: signature.to_string(), args }),
139                return_data: Some(return_data),
140            });
141        }
142    }
143
144    None
145}
146
147pub(super) trait Precompile {
148    fn address(&self) -> Address;
149    fn signature(&self, data: &[u8]) -> &'static str;
150
151    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
152        Ok(vec![hex::encode_prefixed(data)])
153    }
154
155    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
156        Ok(vec![hex::encode_prefixed(data)])
157    }
158}
159
160// Note: we use the ABI decoder, but this is not necessarily ABI-encoded data. It's just a
161// convenient way to decode the data.
162
163const PRECOMPILES: &[&dyn Precompile] = &[
164    &Ecrecover,
165    &Sha256,
166    &Ripemd160,
167    &Identity,
168    &ModExp,
169    &EcAdd,
170    &Ecmul,
171    &Ecpairing,
172    &Blake2f,
173    &PointEvaluation,
174    &Bls12G1Add,
175    &Bls12G1Msm,
176    &Bls12G2Add,
177    &Bls12G2Msm,
178    &Bls12PairingCheck,
179    &Bls12MapFpToG1,
180    &Bls12MapFp2ToG2,
181    &P256Verify,
182];
183
184struct Ecrecover;
185impl Precompile for Ecrecover {
186    fn address(&self) -> Address {
187        EC_RECOVER
188    }
189
190    fn signature(&self, _: &[u8]) -> &'static str {
191        ecrecoverCall::SIGNATURE
192    }
193
194    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
195        let ecrecoverCall { hash, v, r, s } = ecrecoverCall::abi_decode_raw(data)?;
196        Ok(vec![hash.to_string(), v.to_string(), r.to_string(), s.to_string()])
197    }
198
199    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
200        let ret = ecrecoverCall::abi_decode_returns(data)?;
201        Ok(vec![ret.to_string()])
202    }
203}
204
205struct Sha256;
206impl Precompile for Sha256 {
207    fn address(&self) -> Address {
208        SHA_256
209    }
210
211    fn signature(&self, _: &[u8]) -> &'static str {
212        sha256Call::SIGNATURE
213    }
214
215    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
216        let ret = sha256Call::abi_decode_returns(data)?;
217        Ok(vec![ret.to_string()])
218    }
219}
220
221struct Ripemd160;
222impl Precompile for Ripemd160 {
223    fn address(&self) -> Address {
224        RIPEMD_160
225    }
226
227    fn signature(&self, _: &[u8]) -> &'static str {
228        ripemdCall::SIGNATURE
229    }
230
231    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
232        let ret = ripemdCall::abi_decode_returns(data)?;
233        Ok(vec![ret.to_string()])
234    }
235}
236
237struct Identity;
238impl Precompile for Identity {
239    fn address(&self) -> Address {
240        IDENTITY
241    }
242
243    fn signature(&self, _: &[u8]) -> &'static str {
244        identityCall::SIGNATURE
245    }
246}
247
248struct ModExp;
249impl Precompile for ModExp {
250    fn address(&self) -> Address {
251        MOD_EXP
252    }
253
254    fn signature(&self, _: &[u8]) -> &'static str {
255        modexpCall::SIGNATURE
256    }
257
258    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
259        let mut decoder = abi::Decoder::new(data);
260        let b_size = decoder.take_offset()?;
261        let e_size = decoder.take_offset()?;
262        let m_size = decoder.take_offset()?;
263        let b = decoder.take_slice(b_size)?;
264        let e = decoder.take_slice(e_size)?;
265        let m = decoder.take_slice(m_size)?;
266        Ok(vec![
267            b_size.to_string(),
268            e_size.to_string(),
269            m_size.to_string(),
270            hex::encode_prefixed(b),
271            hex::encode_prefixed(e),
272            hex::encode_prefixed(m),
273        ])
274    }
275}
276
277struct EcAdd;
278impl Precompile for EcAdd {
279    fn address(&self) -> Address {
280        EC_ADD
281    }
282
283    fn signature(&self, _: &[u8]) -> &'static str {
284        ecaddCall::SIGNATURE
285    }
286
287    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
288        let ecaddCall { x1, y1, x2, y2 } = ecaddCall::abi_decode_raw(data)?;
289        Ok(vec![x1.to_string(), y1.to_string(), x2.to_string(), y2.to_string()])
290    }
291
292    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
293        let ecaddReturn { x, y } = ecaddCall::abi_decode_returns(data)?;
294        Ok(vec![x.to_string(), y.to_string()])
295    }
296}
297
298struct Ecmul;
299impl Precompile for Ecmul {
300    fn address(&self) -> Address {
301        EC_MUL
302    }
303
304    fn signature(&self, _: &[u8]) -> &'static str {
305        ecmulCall::SIGNATURE
306    }
307
308    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
309        let ecmulCall { x1, y1, s } = ecmulCall::abi_decode_raw(data)?;
310        Ok(vec![x1.to_string(), y1.to_string(), s.to_string()])
311    }
312
313    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
314        let ecmulReturn { x, y } = ecmulCall::abi_decode_returns(data)?;
315        Ok(vec![x.to_string(), y.to_string()])
316    }
317}
318
319struct Ecpairing;
320impl Precompile for Ecpairing {
321    fn address(&self) -> Address {
322        EC_PAIRING
323    }
324
325    fn signature(&self, _: &[u8]) -> &'static str {
326        ecpairingCall::SIGNATURE
327    }
328
329    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
330        let mut decoder = abi::Decoder::new(data);
331        let mut values = Vec::new();
332        // input must be either empty or a multiple of 6 32-byte values
333        let mut tmp = <[&B256; 6]>::default();
334        while !decoder.is_empty() {
335            for tmp in &mut tmp {
336                *tmp = decoder.take_word()?;
337            }
338            values.push(iter_to_string(tmp.iter().map(|x| U256::from_be_bytes(x.0))));
339        }
340        Ok(values)
341    }
342
343    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
344        let ret = ecpairingCall::abi_decode_returns(data)?;
345        Ok(vec![ret.to_string()])
346    }
347}
348
349struct Blake2f;
350impl Precompile for Blake2f {
351    fn address(&self) -> Address {
352        BLAKE_2F
353    }
354
355    fn signature(&self, _: &[u8]) -> &'static str {
356        blake2fCall::SIGNATURE
357    }
358
359    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
360        decode_blake2f(data)
361    }
362}
363
364fn decode_blake2f<'a>(data: &'a [u8]) -> alloy_sol_types::Result<Vec<String>> {
365    let mut decoder = abi::Decoder::new(data);
366    let rounds = u32::from_be_bytes(decoder.take_slice(4)?.try_into().unwrap());
367    let u64_le_list =
368        |x: &'a [u8]| x.chunks_exact(8).map(|x| u64::from_le_bytes(x.try_into().unwrap()));
369    let h = u64_le_list(decoder.take_slice(64)?);
370    let m = u64_le_list(decoder.take_slice(128)?);
371    let t = u64_le_list(decoder.take_slice(16)?);
372    let f = decoder.take_slice(1)?[0];
373    Ok(vec![
374        rounds.to_string(),
375        iter_to_string(h),
376        iter_to_string(m),
377        iter_to_string(t),
378        f.to_string(),
379    ])
380}
381
382struct PointEvaluation;
383impl Precompile for PointEvaluation {
384    fn address(&self) -> Address {
385        POINT_EVALUATION
386    }
387
388    fn signature(&self, _: &[u8]) -> &'static str {
389        pointEvaluationCall::SIGNATURE
390    }
391
392    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
393        let mut decoder = abi::Decoder::new(data);
394        let versioned_hash = decoder.take_word()?;
395        let z = decoder.take_word()?;
396        let y = decoder.take_word()?;
397        let commitment = decoder.take_slice(48)?;
398        let proof = decoder.take_slice(48)?;
399        Ok(vec![
400            versioned_hash.to_string(),
401            z.to_string(),
402            y.to_string(),
403            hex::encode_prefixed(commitment),
404            hex::encode_prefixed(proof),
405        ])
406    }
407}
408
409fn iter_to_string<I: Iterator<Item = T>, T: std::fmt::Display>(iter: I) -> String {
410    format!("[{}]", iter.format(", "))
411}
412
413const G1_POINT_SIZE: usize = 128;
414const G2_POINT_SIZE: usize = 256;
415const SCALAR_SIZE: usize = 32;
416const FP_SIZE: usize = 64;
417
418struct Bls12G1Add;
419impl Precompile for Bls12G1Add {
420    fn address(&self) -> Address {
421        BLS12_G1ADD
422    }
423
424    fn signature(&self, _: &[u8]) -> &'static str {
425        bls12G1AddCall::SIGNATURE
426    }
427
428    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
429        let (p1, rest) = take_at_most(data, G1_POINT_SIZE);
430        let (p2, _) = take_at_most(rest, G1_POINT_SIZE);
431        Ok(vec![hex::encode_prefixed(p1), hex::encode_prefixed(p2)])
432    }
433}
434
435struct Bls12G1Msm;
436impl Precompile for Bls12G1Msm {
437    fn address(&self) -> Address {
438        BLS12_G1MSM
439    }
440
441    fn signature(&self, _: &[u8]) -> &'static str {
442        bls12G1MsmCall::SIGNATURE
443    }
444
445    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
446        let pair_size = G1_POINT_SIZE + SCALAR_SIZE;
447        Ok(data.chunks(pair_size).map(hex::encode_prefixed).collect())
448    }
449}
450
451struct Bls12G2Add;
452impl Precompile for Bls12G2Add {
453    fn address(&self) -> Address {
454        BLS12_G2ADD
455    }
456
457    fn signature(&self, _: &[u8]) -> &'static str {
458        bls12G2AddCall::SIGNATURE
459    }
460
461    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
462        let (p1, rest) = take_at_most(data, G2_POINT_SIZE);
463        let (p2, _) = take_at_most(rest, G2_POINT_SIZE);
464        Ok(vec![hex::encode_prefixed(p1), hex::encode_prefixed(p2)])
465    }
466}
467
468struct Bls12G2Msm;
469impl Precompile for Bls12G2Msm {
470    fn address(&self) -> Address {
471        BLS12_G2MSM
472    }
473
474    fn signature(&self, _: &[u8]) -> &'static str {
475        bls12G2MsmCall::SIGNATURE
476    }
477
478    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
479        let pair_size = G2_POINT_SIZE + SCALAR_SIZE;
480        Ok(data.chunks(pair_size).map(hex::encode_prefixed).collect())
481    }
482}
483
484struct Bls12PairingCheck;
485impl Precompile for Bls12PairingCheck {
486    fn address(&self) -> Address {
487        BLS12_PAIRING_CHECK
488    }
489
490    fn signature(&self, _: &[u8]) -> &'static str {
491        bls12PairingCheckCall::SIGNATURE
492    }
493
494    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
495        let pair_size = G1_POINT_SIZE + G2_POINT_SIZE;
496        Ok(data.chunks(pair_size).map(hex::encode_prefixed).collect())
497    }
498
499    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
500        let ret = bls12PairingCheckCall::abi_decode_returns(data)?;
501        Ok(vec![ret.to_string()])
502    }
503}
504
505struct Bls12MapFpToG1;
506impl Precompile for Bls12MapFpToG1 {
507    fn address(&self) -> Address {
508        BLS12_MAP_FP_TO_G1
509    }
510
511    fn signature(&self, _: &[u8]) -> &'static str {
512        bls12MapFpToG1Call::SIGNATURE
513    }
514
515    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
516        let (fp, _) = take_at_most(data, FP_SIZE);
517        Ok(vec![hex::encode_prefixed(fp)])
518    }
519}
520
521struct Bls12MapFp2ToG2;
522impl Precompile for Bls12MapFp2ToG2 {
523    fn address(&self) -> Address {
524        BLS12_MAP_FP2_TO_G2
525    }
526
527    fn signature(&self, _: &[u8]) -> &'static str {
528        bls12MapFp2ToG2Call::SIGNATURE
529    }
530
531    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
532        let (fp2, _) = take_at_most(data, G1_POINT_SIZE);
533        Ok(vec![hex::encode_prefixed(fp2)])
534    }
535}
536
537struct P256Verify;
538impl Precompile for P256Verify {
539    fn address(&self) -> Address {
540        P256_VERIFY
541    }
542
543    fn signature(&self, _: &[u8]) -> &'static str {
544        p256VerifyCall::SIGNATURE
545    }
546
547    fn decode_call(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
548        let p256VerifyCall { hash, r, s, qx, qy } = p256VerifyCall::abi_decode_raw(data)?;
549        Ok(vec![hash.to_string(), r.to_string(), s.to_string(), qx.to_string(), qy.to_string()])
550    }
551
552    fn decode_return(&self, data: &[u8]) -> alloy_sol_types::Result<Vec<String>> {
553        let ret = p256VerifyCall::abi_decode_returns(data)?;
554        Ok(vec![ret.to_string()])
555    }
556}
557
558fn take_at_most(data: &[u8], n: usize) -> (&[u8], &[u8]) {
559    let n = n.min(data.len());
560    data.split_at(n)
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use alloy_primitives::hex;
567
568    #[test]
569    fn ecpairing() {
570        // https://github.com/foundry-rs/foundry/issues/5337#issuecomment-1627384480
571        let data = hex!(
572            "
573            26bbb723f965460ca7282cd75f0e3e7c67b15817f7cee60856b394936ed02917
574            0fbe873ac672168143a91535450bab6c412dce8dc8b66a88f2da6e245f9282df
575            13cd4f0451538ece5014fe6688b197aefcc611a5c6a7c319f834f2188ba04b08
576            126ff07e81490a1b6ae92b2d9e700c8e23e9d5c7f6ab857027213819a6c9ae7d
577            04183624c9858a56c54deb237c26cb4355bc2551312004e65fc5b299440b15a3
578            2e4b11aa549ad6c667057b18be4f4437fda92f018a59430ebb992fa3462c9ca1
579            2d4d9aa7e302d9df41749d5507949d05dbea33fbb16c643b22f599a2be6df2e2
580            14bedd503c37ceb061d8ec60209fe345ce89830a19230301f076caff004d1926
581            0967032fcbf776d1afc985f88877f182d38480a653f2decaa9794cbc3bf3060c
582            0e187847ad4c798374d0d6732bf501847dd68bc0e071241e0213bc7fc13db7ab
583            304cfbd1e08a704a99f5e847d93f8c3caafddec46b7a0d379da69a4d112346a7
584            1739c1b1a457a8c7313123d24d2f9192f896b7c63eea05a9d57f06547ad0cec8
585            001d6fedb032f70e377635238e0563f131670001f6abf439adb3a9d5d52073c6
586            1889afe91e4e367f898a7fcd6464e5ca4e822fe169bccb624f6aeb87e4d060bc
587            198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2
588            1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed
589            090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b
590            12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa
591            2dde6d7baf0bfa09329ec8d44c38282f5bf7f9ead1914edd7dcaebb498c84519
592            0c359f868a85c6e6c1ea819cfab4a867501a3688324d74df1fe76556558b1937
593            29f41c6e0e30802e2749bfb0729810876f3423e6f24829ad3e30adb1934f1c8a
594            030e7a5f70bb5daa6e18d80d6d447e772efb0bb7fb9d0ffcd54fc5a48af1286d
595            0ea726b117e48cda8bce2349405f006a84cdd3dcfba12efc990df25970a27b6d
596            30364cd4f8a293b1a04f0153548d3e01baad091c69097ca4e9f26be63e4095b5
597        "
598        );
599        let decoded = Ecpairing.decode_call(&data).unwrap();
600        // 4 arrays of 6 32-byte values
601        assert_eq!(decoded.len(), 4);
602    }
603}