diff --git a/Cargo.lock b/Cargo.lock index f572969a7..34b4d992f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1995,6 +1995,7 @@ dependencies = [ "secp256k1", "serde", "serde_json", + "sha2", "sov-modules-api", "sov-prover-storage-manager", "sov-rollup-interface", diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 51f8ad5fe..8a7e43a30 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -49,7 +49,7 @@ reth-rpc-eth-types = { workspace = true, optional = true } reth-rpc-server-types = { workspace = true, optional = true } reth-rpc-types-compat = { workspace = true, optional = true } reth-transaction-pool = { workspace = true, optional = true } -revm = { workspace = true, features = ["secp256k1"] } +revm = { workspace = true, default-features = false, features = ["secp256k1"] } revm-inspectors = { workspace = true, optional = true } secp256k1 = { workspace = true, optional = true } @@ -64,7 +64,8 @@ rayon = { workspace = true } reth-chainspec = { workspace = true } reth-db = { workspace = true } reth-errors = { workspace = true } -revm = { workspace = true, features = ["optional_block_gas_limit", "optional_eip3607", "optional_no_base_fee", "secp256k1"] } +revm = { workspace = true, default-features = false, features = ["optional_block_gas_limit", "optional_eip3607", "optional_no_base_fee", "secp256k1"] } +sha2 = { workspace = true } sov-modules-api = { path = "../sovereign-sdk/module-system/sov-modules-api", features = ["macros"] } sov-prover-storage-manager = { path = "../sovereign-sdk/full-node/sov-prover-storage-manager", features = ["test-utils"] } sov-rollup-interface = { path = "../sovereign-sdk/rollup-interface", features = ["testing"] } @@ -84,6 +85,7 @@ native = [ "reth-rpc-eth-api", "reth-rpc-server-types", "reth-rpc-types-compat", + "revm/serde", "reth-transaction-pool", "revm-inspectors", "reth-provider", diff --git a/crates/evm/src/evm/handler.rs b/crates/evm/src/evm/handler.rs index 16fa41639..a323e3110 100644 --- a/crates/evm/src/evm/handler.rs +++ b/crates/evm/src/evm/handler.rs @@ -8,13 +8,14 @@ use revm::handler::register::{EvmHandler, HandleRegisters}; #[cfg(feature = "native")] use revm::interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter}; use revm::interpreter::{Gas, InstructionResult}; +use revm::precompile::u64_to_address; #[cfg(feature = "native")] use revm::primitives::Log; use revm::primitives::{ spec_to_generic, Address, EVMError, Env, HandlerCfg, InvalidTransaction, ResultAndState, Spec, SpecId, B256, U256, }; -use revm::{Context, Database, FrameResult, InnerEvmContext, JournalEntry}; +use revm::{Context, ContextPrecompiles, Database, FrameResult, InnerEvmContext, JournalEntry}; #[cfg(feature = "native")] use revm::{EvmContext, Inspector}; use sov_modules_api::{native_debug, native_error, native_warn}; @@ -290,7 +291,7 @@ where // validation.env = validation.tx_against_state = Arc::new(CitreaHandler::::validate_tx_against_state); - // pre_execution.load_accounts = + pre_execution.load_precompiles = Arc::new(CitreaHandler::::load_precompiles); // pre_execution.load_accounts = pre_execution.deduct_caller = Arc::new(CitreaHandler::::deduct_caller); // execution.last_frame_return = @@ -314,6 +315,22 @@ struct CitreaHandler { } impl CitreaHandler { + fn load_precompiles() -> ContextPrecompiles { + fn our_precompiles() -> ContextPrecompiles { + let mut precompiles = revm::handler::mainnet::load_precompiles::(); + + if SPEC::enabled(SpecId::CANCUN) { + precompiles + .to_mut() + .remove(&u64_to_address(0x0A)) + .expect("after cancun point eval should be removed"); + } + + precompiles + } + + our_precompiles::() + } fn validate_tx_against_state( context: &mut Context, ) -> Result<(), EVMError> { diff --git a/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.abi b/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.abi new file mode 100644 index 000000000..f5bbbc1ef --- /dev/null +++ b/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"bytes","name":"input","type":"bytes"}],"name":"verifyPointEvaluation","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.bin b/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.bin new file mode 100644 index 000000000..6afea8cd5 --- /dev/null +++ b/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.bin @@ -0,0 +1 @@ +6080604052348015600e575f5ffd5b506103208061001c5f395ff3fe608060405234801561000f575f5ffd5b5060043610610029575f3560e01c806325a05f401461002d575b5f5ffd5b610047600480360381019061004291906101a0565b61005d565b6040516100549190610205565b60405180910390f35b5f60c083839050146100a4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161009b90610278565b60405180910390fd5b6060600a73ffffffffffffffffffffffffffffffffffffffff1684846040516100ce9291906102d2565b5f60405180830381855afa9150503d805f8114610106576040519150601f19603f3d011682016040523d82523d5f602084013e61010b565b606091505b5080925081935050505f815114610120575f5ffd5b60408101515f5581610130575f5ffd5b5092915050565b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5ffd5b5f5f83601f8401126101605761015f61013f565b5b8235905067ffffffffffffffff81111561017d5761017c610143565b5b60208301915083600182028301111561019957610198610147565b5b9250929050565b5f5f602083850312156101b6576101b5610137565b5b5f83013567ffffffffffffffff8111156101d3576101d261013b565b5b6101df8582860161014b565b92509250509250929050565b5f8115159050919050565b6101ff816101eb565b82525050565b5f6020820190506102185f8301846101f6565b92915050565b5f82825260208201905092915050565b7f496e76616c696420696e7075742073697a6500000000000000000000000000005f82015250565b5f61026260128361021e565b915061026d8261022e565b602082019050919050565b5f6020820190508181035f83015261028f81610256565b9050919050565b5f81905092915050565b828183375f83830152505050565b5f6102b98385610296565b93506102c68385846102a0565b82840190509392505050565b5f6102de8284866102ae565b9150819050939250505056fea264697066735822122052f3d3eafaae24a10a4cb029d517a2fc7b949c9ee79750ca303d13aba9fb158264736f6c634300081b0033 \ No newline at end of file diff --git a/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.sol b/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.sol new file mode 100644 index 000000000..970b75635 --- /dev/null +++ b/crates/evm/src/evm/test_data/KZGPointEvaluationCaller.sol @@ -0,0 +1,21 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract KZGPointEvaluationCaller { + /// @notice Calls the 0x0A precompile to perform point evaluation + /// @param input A 192-byte input representing the polynomial versioned hash, commitment, point, and proof + function verifyPointEvaluation( + bytes calldata input // 192 bytes + ) external returns (bool success) { + require(input.length == 192, "Invalid input size"); + bytes memory out; + (success, out) = address(10).staticcall(input); + require(out.length == 0); + // Write the 32 bytes of out to first storage slot + assembly { + sstore(0, mload(add(out, 64))) + } + require(success); + } +} \ No newline at end of file diff --git a/crates/evm/src/smart_contracts/kzg_point_evaluation_caller.rs b/crates/evm/src/smart_contracts/kzg_point_evaluation_caller.rs new file mode 100644 index 000000000..551e56b43 --- /dev/null +++ b/crates/evm/src/smart_contracts/kzg_point_evaluation_caller.rs @@ -0,0 +1,49 @@ +use alloy_primitives::Bytes; +use alloy_sol_types::{sol, SolCall}; + +use super::TestContract; + +// KZGPointEvaluationCallerContract wrapper. +sol! { + #[sol(abi)] + KZGPointEvaluationCaller, + "./src/evm/test_data/KZGPointEvaluationCaller.abi" +} + +/// KZGPointEvaluationContract wrapper. +pub struct KZGPointEvaluationCallerContract { + bytecode: Vec, +} + +impl Default for KZGPointEvaluationCallerContract { + fn default() -> Self { + let bytecode = { + let bytecode_hex = + include_str!("../../../evm/src/evm/test_data/KZGPointEvaluationCaller.bin"); + hex::decode(bytecode_hex).unwrap() + }; + + Self { bytecode } + } +} + +impl TestContract for KZGPointEvaluationCallerContract { + fn byte_code(&self) -> Vec { + self.byte_code() + } +} + +impl KZGPointEvaluationCallerContract { + /// KZGPointEvaluation bytecode. + pub fn byte_code(&self) -> Vec { + self.bytecode.clone() + } + + /// Claims the gift. + pub fn call_kzg_point_evaluation( + &self, + input: Bytes, // 192 bytes + ) -> Vec { + KZGPointEvaluationCaller::verifyPointEvaluationCall { input }.abi_encode() + } +} diff --git a/crates/evm/src/smart_contracts/mod.rs b/crates/evm/src/smart_contracts/mod.rs index d32c43648..d35dee325 100644 --- a/crates/evm/src/smart_contracts/mod.rs +++ b/crates/evm/src/smart_contracts/mod.rs @@ -6,6 +6,7 @@ mod caller_contract; mod coinbase_contract; mod hive_contract; mod infinite_loop_contract; +mod kzg_point_evaluation_caller; mod logs_contract; mod mcopy_contract; mod payable_contract; @@ -20,6 +21,7 @@ pub use caller_contract::CallerContract; pub use coinbase_contract::CoinbaseContract; pub use hive_contract::HiveContract; pub use infinite_loop_contract::InfiniteLoopContract; +pub use kzg_point_evaluation_caller::KZGPointEvaluationCallerContract; pub use logs_contract::{AnotherLogEvent, LogEvent, LogsContract}; pub use mcopy_contract::McopyContract; pub use payable_contract::SimplePayableContract; diff --git a/crates/evm/src/tests/fork_tests.rs b/crates/evm/src/tests/fork_tests.rs index 07051b6af..f117f5b8c 100644 --- a/crates/evm/src/tests/fork_tests.rs +++ b/crates/evm/src/tests/fork_tests.rs @@ -1,7 +1,9 @@ use std::str::FromStr; +use std::thread::sleep; -use alloy_primitives::{address, keccak256, Address, TxKind}; +use alloy_primitives::{address, keccak256, Address, Bytes, TxKind}; use revm::primitives::U256; +use sha2::Digest; use sov_modules_api::default_context::DefaultContext; use sov_modules_api::hooks::HookSoftConfirmationInfo; use sov_modules_api::utils::generate_address; @@ -11,17 +13,19 @@ use sov_rollup_interface::spec::SpecId as SovSpecId; use crate::call::CallMessage; use crate::evm::DbAccount; use crate::smart_contracts::{ - BlobBaseFeeContract, McopyContract, SelfdestructingConstructorContract, - TransientStorageContract, + BlobBaseFeeContract, KZGPointEvaluationCallerContract, McopyContract, SelfDestructorContract, + SelfdestructingConstructorContract, SimpleStorageContract, TransientStorageContract, }; use crate::tests::test_signer::TestSigner; -use crate::tests::utils::{create_contract_message, get_evm, get_evm_config}; +use crate::tests::utils::{create_contract_message, get_evm, get_evm_config, set_arg_message}; use crate::RlpEvmTransaction; type C = DefaultContext; use super::call_tests::send_money_to_contract_message; use super::utils::create_contract_message_with_bytecode; +const VERSIONED_HASH_VERSION_KZG: u8 = 1; + fn claim_gift_from_transient_storage_contract_transaction( contract_addr: Address, dev_signer: &TestSigner, @@ -56,6 +60,23 @@ fn store_blob_base_fee_transaction( .unwrap() } +fn call_kzg_point_evaluation_transaction( + contract_addr: Address, + dev_signer: &TestSigner, + nonce: u64, + input: Bytes, +) -> RlpEvmTransaction { + let contract = KZGPointEvaluationCallerContract::default(); + dev_signer + .sign_default_transaction( + TxKind::Call(contract_addr), + contract.call_kzg_point_evaluation(input), + nonce, + 0, + ) + .unwrap() +} + #[test] fn test_cancun_transient_storage_activation() { let (config, dev_signer, contract_addr) = @@ -234,7 +255,6 @@ fn test_cancun_mcopy_activation() { timestamp: 0, }; - // Deploy transient storage contract let sender_address = generate_address::("sender"); evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); { @@ -436,7 +456,6 @@ fn test_blob_base_fee_should_return_1() { timestamp: 0, }; - // Deploy transient storage contract let sender_address = generate_address::("sender"); evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); { @@ -535,3 +554,292 @@ fn test_blob_base_fee_should_return_1() { assert_eq!(storage_value, U256::from(1)); } + +#[test] +fn test_kzg_point_eval_should_revert() { + let (config, dev_signer, contract_addr) = + get_evm_config(U256::from_str("100000000000000000000").unwrap(), None); + + let (mut evm, mut working_set) = get_evm(&config); + let l1_fee_rate = 0; + let mut l2_height = 2; + + let soft_confirmation_info = HookSoftConfirmationInfo { + l2_height, + da_slot_hash: [5u8; 32], + da_slot_height: 1, + da_slot_txs_commitment: [42u8; 32], + pre_state_root: [10u8; 32].to_vec(), + current_spec: SovSpecId::Fork1, + pub_key: vec![], + deposit_data: vec![], + l1_fee_rate, + timestamp: 0, + }; + + let sender_address = generate_address::("sender"); + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let context = C::new(sender_address, l2_height, SovSpecId::Fork1, l1_fee_rate); + + let deploy_message = + create_contract_message(&dev_signer, 0, KZGPointEvaluationCallerContract::default()); + + evm.call( + CallMessage { + txs: vec![deploy_message], + }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + + l2_height += 1; + + // Implementation taken from https://eips.ethereum.org/EIPS/eip-4844#point-evaluation-precompile + fn kzg_to_versioned_hash(commitment: Bytes) -> Bytes { + let mut commitment_hash = sha2::Sha256::digest(commitment).to_vec(); + commitment_hash[0] = VERSIONED_HASH_VERSION_KZG; + Bytes::from(commitment_hash) + } + + // data is taken from: https://github.com/ethereum/c-kzg-4844/tree/main/tests/verify_kzg_proof/kzg-mainnet/verify_kzg_proof_case_correct_proof_d0992bc0387790a4 + let commitment= Bytes::from_str("8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7").unwrap(); + let versioned_hash = kzg_to_versioned_hash(commitment.clone()); + let z = Bytes::from_str("5eb7004fe57383e6c88b99d839937fddf3f99279353aaf8d5c9a75f91ce33c62") + .unwrap(); + let y = Bytes::from_str("4882cf0609af8c7cd4c256e63a35838c95a9ebbf6122540ab344b42fd66d32e1") + .unwrap(); + let proof = Bytes::from_str("0x987ea6df69bbe97c23e0dd948cf2d4490824ba7fea5af812721b2393354b0810a9dba2c231ea7ae30f26c412c7ea6e3a").unwrap(); + + // The data is encoded as follows: versioned_hash | z | y | commitment | proof | with z and y being padded 32 byte big endian values + // ref: https://eips.ethereum.org/EIPS/eip-4844#point-evaluation-precompile + let mut input = vec![]; + input.extend_from_slice(&versioned_hash); + input.extend_from_slice(&z); + input.extend_from_slice(&y); + input.extend_from_slice(&commitment); + input.extend_from_slice(&proof); + + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let context = C::new(sender_address, l2_height, SovSpecId::Fork1, l1_fee_rate); + + let deploy_message = call_kzg_point_evaluation_transaction( + contract_addr, + &dev_signer, + 1, + Bytes::from(input), + ); + + evm.call( + CallMessage { + txs: vec![deploy_message], + }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + + // expect this call to fail because we do not have the kzg feature of revm enabled on fork1 + let receipts: Vec<_> = evm + .receipts + .iter(&mut working_set.accessory_state()) + .collect(); + + let db_account = DbAccount::new(contract_addr); + let storage_value = db_account + .storage + .get(&U256::ZERO, &mut working_set) + .unwrap(); + assert_ne!( + storage_value, + // expected if point eval precompile was enabled + U256::from_str( + "52435875175126190479447740508185965837690552500527637822603658699938581184513" + ) + .unwrap() + ); + assert!(receipts.last().unwrap().receipt.success); +} + +#[test] +fn test_offchain_contract_storage_evm() { + let (config, dev_signer, contract_addr) = + get_evm_config(U256::from_str("100000000000000000000").unwrap(), None); + + let (mut evm, mut working_set) = get_evm(&config); + let l1_fee_rate = 0; + let mut l2_height = 2; + + // Deployed a contract in genesis fork + let soft_confirmation_info = HookSoftConfirmationInfo { + l2_height, + da_slot_hash: [5u8; 32], + da_slot_height: 1, + da_slot_txs_commitment: [42u8; 32], + pre_state_root: [10u8; 32].to_vec(), + current_spec: SovSpecId::Genesis, + pub_key: vec![], + deposit_data: vec![], + l1_fee_rate, + timestamp: 0, + }; + + let sender_address = generate_address::("sender"); + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let context = C::new(sender_address, l2_height, SovSpecId::Genesis, l1_fee_rate); + + let deploy_message = + create_contract_message(&dev_signer, 0, SimpleStorageContract::default()); + + evm.call( + CallMessage { + txs: vec![deploy_message], + }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + + l2_height += 1; + + sleep(std::time::Duration::from_secs(2)); + + //try to get it from offchain storage and expect it to not exist + let contract_info = evm.accounts.get(&contract_addr, &mut working_set); + let code_hash = contract_info.unwrap().code_hash.unwrap(); + + let offchain_code = evm + .offchain_code + .get(&code_hash, &mut working_set.offchain_state()); + + assert!(offchain_code.is_none()); + + // activate fork and then try to get it from offchain storage and expect it to exist + // Deployed a contract in genesis fork + let soft_confirmation_info = HookSoftConfirmationInfo { + l2_height, + da_slot_hash: [5u8; 32], + da_slot_height: 1, + da_slot_txs_commitment: [42u8; 32], + pre_state_root: [10u8; 32].to_vec(), + current_spec: SovSpecId::Fork1, + pub_key: vec![], + deposit_data: vec![], + l1_fee_rate, + timestamp: 0, + }; + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + sleep(std::time::Duration::from_secs(2)); + l2_height += 1; + + let offchain_code = evm + .offchain_code + .get(&code_hash, &mut working_set.offchain_state()); + + assert!(offchain_code.is_none()); + + let evm_code = evm.code.get(&code_hash, &mut working_set).unwrap(); + + let code = evm + .get_code( + contract_addr, + Some(alloy_eips::BlockId::Number( + alloy_eips::BlockNumberOrTag::Latest, + )), + &mut working_set, + ) + .unwrap(); + + assert_eq!(code, evm_code.original_bytes()); + + // Deploy contract in fork1 + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let context = C::new(sender_address, l2_height, SovSpecId::Fork1, l1_fee_rate); + + let deploy_message = + create_contract_message(&dev_signer, 1, SelfDestructorContract::default()); + + evm.call( + CallMessage { + txs: vec![deploy_message], + }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + l2_height += 1; + + let new_contract_address = address!("d26ff5586e488e65d86bcc3f0fe31551e381a596"); + + let contract_info = evm.accounts.get(&new_contract_address, &mut working_set); + let code_hash = contract_info.unwrap().code_hash.unwrap(); + + let offchain_code = evm + .offchain_code + .get(&code_hash, &mut working_set.offchain_state()); + + assert!(offchain_code.is_some()); + + let evm_code = evm.code.get(&code_hash, &mut working_set); + assert!(evm_code.is_none()); + + // make tx on the contract that was deployed before fork1 and see that you can read it from offchain storage afterwards + let soft_confirmation_info = HookSoftConfirmationInfo { + l2_height, + da_slot_hash: [5u8; 32], + da_slot_height: 1, + da_slot_txs_commitment: [42u8; 32], + pre_state_root: [10u8; 32].to_vec(), + current_spec: SovSpecId::Fork1, + pub_key: vec![], + deposit_data: vec![], + l1_fee_rate, + timestamp: 0, + }; + + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let context = C::new(sender_address, l2_height, SovSpecId::Fork1, l1_fee_rate); + + let call_message = set_arg_message(contract_addr, &dev_signer, 2, 99); + + evm.call( + CallMessage { + txs: vec![call_message], + }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + + // Now I should be able to read the contract from offchain storage + let contract_info = evm.accounts.get(&contract_addr, &mut working_set); + let code_hash = contract_info.unwrap().code_hash.unwrap(); + + let offchain_code = evm + .offchain_code + .get(&code_hash, &mut working_set.offchain_state()); + + assert!(offchain_code.is_some()); +} diff --git a/guests/risc0/batch-proof-bitcoin/Cargo.lock b/guests/risc0/batch-proof-bitcoin/Cargo.lock index 7c1f3f91b..fce7a3dd3 100644 --- a/guests/risc0/batch-proof-bitcoin/Cargo.lock +++ b/guests/risc0/batch-proof-bitcoin/Cargo.lock @@ -251,7 +251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa64d80ae58ffaafdff9d5d84f58d03775f66c84433916dc9a64ed16af5755da" dependencies = [ "serde", - "winnow 0.6.13", + "winnow 0.6.20", ] [[package]] @@ -3861,9 +3861,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" [[package]] name = "wyz"