diff --git a/Cargo.lock b/Cargo.lock index 1b2f1ed97137e..be021396082a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2618,8 +2618,10 @@ dependencies = [ "aptos-metrics-core", "aptos-types", "ark-bls12-381", + "ark-bn254", "ark-ec", "ark-ff", + "ark-groth16", "ark-serialize", "bcs 0.1.4", "dashmap", diff --git a/aptos-move/e2e-move-tests/README.md b/aptos-move/e2e-move-tests/README.md new file mode 100644 index 0000000000000..f025e0ca31a9b --- /dev/null +++ b/aptos-move/e2e-move-tests/README.md @@ -0,0 +1,9 @@ +# e2e-move-tests + +## Keyless + +To run the keyless VM tests: + +``` +cargo test -- keyless +``` diff --git a/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs b/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs index 08d283cd67feb..2524f6fdffa3d 100644 --- a/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs +++ b/aptos-move/e2e-move-tests/src/tests/keyless_feature_gating.rs @@ -16,6 +16,7 @@ use aptos_types::{ }, AnyKeylessPublicKey, Configuration, EphemeralCertificate, FederatedKeylessPublicKey, Groth16VerificationKey, KeylessPublicKey, KeylessSignature, TransactionAndProof, + VERIFICATION_KEY_FOR_TESTING, }, on_chain_config::FeatureFlag, transaction::{ @@ -35,7 +36,7 @@ use move_core_types::{ }, }; -/// Initializes an Aptos VM and sets the keyless configuration via script (the VK is already set in genesis). +/// Initializes an Aptos VM and sets the keyless configuration via script. fn init_feature_gating( enabled_features: Vec, disabled_features: Vec, @@ -46,6 +47,12 @@ fn init_feature_gating( // initialize JWKs let core_resources = run_jwk_and_config_script(&mut h); + // initialize default VK + run_upgrade_vk_script( + &mut h, + core_resources.clone(), + Groth16VerificationKey::from(VERIFICATION_KEY_FOR_TESTING.clone()), + ); (h, recipient, core_resources) } @@ -256,6 +263,14 @@ fn test_federated_keyless_at_jwk_addr() { let jwk_addr = AccountAddress::from_hex_literal("0xadd").unwrap(); + // Step 0: Make sure the default VK is installed + let core_resources = h.new_account_at(AccountAddress::from_hex_literal("0xA550C18").unwrap()); + run_upgrade_vk_script( + &mut h, + core_resources.clone(), + Groth16VerificationKey::from(VERIFICATION_KEY_FOR_TESTING.clone()), + ); + // Step 1: Make sure TXN validation fails if JWKs are not installed at jwk_addr. let (sig, pk) = get_sample_groth16_sig_and_pk(); let sender = create_federated_keyless_account(&mut h, jwk_addr, pk); @@ -280,7 +295,7 @@ fn test_federated_keyless_at_jwk_addr() { // Step 1: Make sure TXN validation succeeds once JWKs are installed at jwk_addr. let iss = get_sample_iss(); let jwk = get_sample_jwk(); - let _core_resources = install_federated_jwks_and_set_keyless_config(&mut h, jwk_addr, iss, jwk); + let _ = install_federated_jwks_and_set_keyless_config(&mut h, jwk_addr, iss, jwk); let txn = spend_keyless_account(&mut h, sig, &sender, *recipient.address()); let output = h.run_raw(txn); @@ -308,7 +323,14 @@ fn test_federated_keyless_override_at_0x1() { let jwk_addr = AccountAddress::from_hex_literal("0xadd").unwrap(); let iss = get_sample_iss(); let jwk = secure_test_rsa_jwk(); // this will be the wrong JWK - let _core_resources = install_federated_jwks_and_set_keyless_config(&mut h, jwk_addr, iss, jwk); + let core_resources = install_federated_jwks_and_set_keyless_config(&mut h, jwk_addr, iss, jwk); + + // Step 0: Make sure the default VK is installed + run_upgrade_vk_script( + &mut h, + core_resources.clone(), + Groth16VerificationKey::from(VERIFICATION_KEY_FOR_TESTING.clone()), + ); // Step 1: Make sure the TXN does not validate, since the wrong JWK is installed at JWK addr let (sig, pk) = get_sample_groth16_sig_and_pk(); @@ -441,7 +463,7 @@ fn create_and_spend_keyless_account( spend_keyless_account(h, sig, &account, recipient) } -/// Sets the keyless configuration (Note: the VK is already set in genesis.) +/// Sets the keyless configuration fn run_jwk_and_config_script(h: &mut MoveHarness) -> Account { let core_resources = h.new_account_at(AccountAddress::from_hex_literal("0xA550C18").unwrap()); @@ -475,16 +497,14 @@ fn run_jwk_and_config_script(h: &mut MoveHarness) -> Account { .sign(); // NOTE: We cannot write the Configuration and Groth16Verification key via MoveHarness::set_resource - // because it does not (yet) work with resource groups. This is okay, because the VK will be - // there from genesis. + // because it does not (yet) work with resource groups. assert_success!(h.run(txn)); core_resources } -/// Sets the keyless configuration and installs the sample RSA JWK as a federated JWK -/// (Note: the VK is already set in genesis.) +/// Sets the keyless configuration and installs the sample RSA JWK as a federated JWK. fn install_federated_jwks_and_set_keyless_config( h: &mut MoveHarness, jwk_owner: AccountAddress, @@ -524,8 +544,7 @@ fn federated_keyless_init_config(h: &mut MoveHarness, core_resources: Account) { .sign(); // NOTE: We cannot write the Configuration and Groth16Verification key via MoveHarness::set_resource - // because it does not (yet) work with resource groups. This is okay, because the VK will be - // there from genesis. + // because it does not (yet) work with resource groups. assert_success!(h.run(txn)); } @@ -557,8 +576,7 @@ fn federated_keyless_install_jwk( .sign(); // NOTE: We cannot write the Configuration and Groth16Verification key via MoveHarness::set_resource - // because it does not (yet) work with resource groups. This is okay, because the VK will be - // there from genesis. + // because it does not (yet) work with resource groups. assert_success!(h.run(txn)); } diff --git a/aptos-move/vm-genesis/src/lib.rs b/aptos-move/vm-genesis/src/lib.rs index 239713079c616..ea57e7b377de2 100644 --- a/aptos-move/vm-genesis/src/lib.rs +++ b/aptos-move/vm-genesis/src/lib.rs @@ -27,8 +27,7 @@ use aptos_types::{ secure_test_rsa_jwk, }, keyless::{ - self, test_utils::get_sample_iss, Groth16VerificationKey, DEVNET_VERIFICATION_KEY, - KEYLESS_ACCOUNT_MODULE_NAME, + self, test_utils::get_sample_iss, Groth16VerificationKey, KEYLESS_ACCOUNT_MODULE_NAME, }, move_utils::as_move_value::AsMoveValue, on_chain_config::{ @@ -111,7 +110,7 @@ pub struct GenesisConfiguration { pub randomness_config_override: Option, pub jwk_consensus_config_override: Option, pub initial_jwks: Vec, - pub keyless_groth16_vk_override: Option, + pub keyless_groth16_vk: Option, } pub static GENESIS_KEYPAIR: Lazy<(Ed25519PrivateKey, Ed25519PublicKey)> = Lazy::new(|| { @@ -312,7 +311,7 @@ pub fn encode_genesis_change_set( &module_storage, chain_id, genesis_config.initial_jwks.clone(), - genesis_config.keyless_groth16_vk_override.clone(), + genesis_config.keyless_groth16_vk.clone(), ); set_genesis_end(&mut session, &module_storage); @@ -686,7 +685,7 @@ fn initialize_keyless_accounts( module_storage: &impl AptosModuleStorage, chain_id: ChainId, mut initial_jwks: Vec, - vk_override: Option, + vk: Option, ) { let config = keyless::Configuration::new_for_devnet(); exec_function( @@ -700,9 +699,8 @@ fn initialize_keyless_accounts( config.as_move_value(), ]), ); - if !chain_id.is_mainnet() { - let vk = - vk_override.unwrap_or_else(|| Groth16VerificationKey::from(&*DEVNET_VERIFICATION_KEY)); + + if vk.is_some() { exec_function( session, module_storage, @@ -711,10 +709,11 @@ fn initialize_keyless_accounts( vec![], serialize_values(&vec![ MoveValue::Signer(CORE_CODE_ADDRESS), - vk.as_move_value(), + vk.unwrap().as_move_value(), ]), ); - + } + if !chain_id.is_mainnet() { let additional_jwk_patch = IssuerJWK { issuer: get_sample_iss(), jwk: JWK::RSA(secure_test_rsa_jwk()), @@ -1255,7 +1254,7 @@ pub fn generate_test_genesis( randomness_config_override: None, jwk_consensus_config_override: None, initial_jwks: vec![], - keyless_groth16_vk_override: None, + keyless_groth16_vk: None, }, &OnChainConsensusConfig::default_for_genesis(), &OnChainExecutionConfig::default_for_genesis(), @@ -1307,7 +1306,7 @@ fn mainnet_genesis_config() -> GenesisConfiguration { randomness_config_override: None, jwk_consensus_config_override: None, initial_jwks: vec![], - keyless_groth16_vk_override: None, + keyless_groth16_vk: None, } } diff --git a/crates/aptos-genesis/src/builder.rs b/crates/aptos-genesis/src/builder.rs index 01921473e7c9b..c3440d5cee217 100644 --- a/crates/aptos-genesis/src/builder.rs +++ b/crates/aptos-genesis/src/builder.rs @@ -444,7 +444,7 @@ pub struct GenesisConfiguration { pub randomness_config_override: Option, pub jwk_consensus_config_override: Option, pub initial_jwks: Vec, - pub keyless_groth16_vk_override: Option, + pub keyless_groth16_vk: Option, } pub type InitConfigFn = Arc; @@ -667,7 +667,7 @@ impl Builder { randomness_config_override: None, jwk_consensus_config_override: None, initial_jwks: vec![], - keyless_groth16_vk_override: None, + keyless_groth16_vk: None, }; if let Some(init_genesis_config) = &self.init_genesis_config { (init_genesis_config)(&mut genesis_config); diff --git a/crates/aptos-genesis/src/lib.rs b/crates/aptos-genesis/src/lib.rs index 8fed37e85cfa6..bcce6731f1e16 100644 --- a/crates/aptos-genesis/src/lib.rs +++ b/crates/aptos-genesis/src/lib.rs @@ -79,7 +79,7 @@ pub struct GenesisInfo { pub randomness_config_override: Option, pub jwk_consensus_config_override: Option, pub initial_jwks: Vec, - pub keyless_groth16_vk_override: Option, + pub keyless_groth16_vk: Option, } impl GenesisInfo { @@ -120,7 +120,7 @@ impl GenesisInfo { randomness_config_override: genesis_config.randomness_config_override.clone(), jwk_consensus_config_override: genesis_config.jwk_consensus_config_override.clone(), initial_jwks: genesis_config.initial_jwks.clone(), - keyless_groth16_vk_override: genesis_config.keyless_groth16_vk_override.clone(), + keyless_groth16_vk: genesis_config.keyless_groth16_vk.clone(), }) } @@ -157,7 +157,7 @@ impl GenesisInfo { randomness_config_override: self.randomness_config_override.clone(), jwk_consensus_config_override: self.jwk_consensus_config_override.clone(), initial_jwks: self.initial_jwks.clone(), - keyless_groth16_vk_override: self.keyless_groth16_vk_override.clone(), + keyless_groth16_vk: self.keyless_groth16_vk.clone(), }, &self.consensus_config, &self.execution_config, diff --git a/crates/aptos-genesis/src/mainnet.rs b/crates/aptos-genesis/src/mainnet.rs index 37ece1845d719..e78e619694ee2 100644 --- a/crates/aptos-genesis/src/mainnet.rs +++ b/crates/aptos-genesis/src/mainnet.rs @@ -144,7 +144,7 @@ impl MainnetGenesisInfo { randomness_config_override: self.randomness_config_override.clone(), jwk_consensus_config_override: self.jwk_consensus_config_override.clone(), initial_jwks: vec![], - keyless_groth16_vk_override: None, + keyless_groth16_vk: None, }, ) } diff --git a/crates/aptos/src/common/types.rs b/crates/aptos/src/common/types.rs index 9667831e814f5..5443a9a936be9 100644 --- a/crates/aptos/src/common/types.rs +++ b/crates/aptos/src/common/types.rs @@ -1608,7 +1608,7 @@ impl FaucetOptions { } /// Gas price options for manipulating how to prioritize transactions -#[derive(Debug, Eq, Parser, PartialEq)] +#[derive(Debug, Clone, Eq, Parser, PartialEq)] pub struct GasOptions { /// Gas multiplier per unit of gas /// diff --git a/crates/aptos/src/genesis/mod.rs b/crates/aptos/src/genesis/mod.rs index 001f9ae1a93ca..5e5fcca4d8ea6 100644 --- a/crates/aptos/src/genesis/mod.rs +++ b/crates/aptos/src/genesis/mod.rs @@ -261,7 +261,7 @@ pub fn fetch_mainnet_genesis_info(git_options: GitOptions) -> CliTypedResult CliTypedResult String { #[tokio::main] async fn main() { + // if let Ok(x) = std::env::var("V0_VERIFY") { + // test_v0_verify(); + // } println!(); println!("Starting an interaction with aptos-oidb-pepper-service."); let url = get_pepper_service_url(); diff --git a/keyless/pepper/readme.md b/keyless/pepper/readme.md index 493650962e0c7..8538641414841 100644 --- a/keyless/pepper/readme.md +++ b/keyless/pepper/readme.md @@ -58,3 +58,32 @@ Follow the instruction to manually complete a session with the pepper service. Sorry for the missing examples in other programming languages. For now please read through `example-client-rust/src/main.rs` implementation and output: that is what your frontend needs to do. + +## Extra: manual testing for endpoint `v0/verify`. +NOTE: API `v0/verify` now depends on on-chain resources +`0x1::keyless_account::Groth16VerificationKey` and `0x1::keyless_account::Configuration`, +which need to be fetched via HTTP requests. + +In terminal 0, run the pepper service. +```bash +export VUF_KEY_SEED_HEX=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +export ONCHAIN_GROTH16_VK_URL=http://localhost:4444/groth16_vk.json +export ONCHAIN_KEYLESS_CONFIG_URL=http://localhost:4444/keyless_config.json +cargo run -p aptos-keyless-pepper-service +``` + +In terminal 1, peek the cached resources, they should currently give 404. +``` +curl -v http://localhost:8000/cached/groth16-vk +curl -v http://localhost:8000/cached/keyless-config +``` + +In terminal 2, mock the full node with a naive HTTP server. +```bash +cd keyless/pepper/service/resources +python3 -m http.server 4444 +``` + +Wait for 10 secs then go back to terminal 1 to retry the curl cmds. The cached data should be available. + +TODO: how to generate sample request and interact with `v0/verify` endpoint? diff --git a/keyless/pepper/service/Cargo.toml b/keyless/pepper/service/Cargo.toml index 3dc8adf9044b9..a5a321d5faae1 100644 --- a/keyless/pepper/service/Cargo.toml +++ b/keyless/pepper/service/Cargo.toml @@ -23,8 +23,10 @@ aptos-logger = { workspace = true } aptos-metrics-core = { workspace = true } aptos-types = { workspace = true } ark-bls12-381 = { workspace = true } +ark-bn254 = { workspace = true } ark-ec = { workspace = true } ark-ff = { workspace = true } +ark-groth16 = { workspace = true } ark-serialize = { workspace = true } bcs = { workspace = true } dashmap = { workspace = true } diff --git a/keyless/pepper/service/resources/groth16_vk.json b/keyless/pepper/service/resources/groth16_vk.json new file mode 100644 index 0000000000000..705f734f75374 --- /dev/null +++ b/keyless/pepper/service/resources/groth16_vk.json @@ -0,0 +1,13 @@ +{ + "type": "0x1::keyless_account::Groth16VerificationKey", + "data": { + "alpha_g1": "0xe2f26dbea299f5223b646cb1fb33eadb059d9407559d7441dfd902e3a79a4d2d", + "beta_g2": "0xabb73dc17fbc13021e2471e0c08bd67d8401f52b73d6d07483794cad4778180e0c06f33bbc4c79a9cadef253a68084d382f17788f885c9afd176f7cb2f036789", + "delta_g2": "0xb106619932d0ef372c46909a2492e246d5de739aa140e27f2c71c0470662f125219049cfe15e4d140d7e4bb911284aad1cad19880efb86f2d9dd4b1bb344ef8f", + "gamma_abc_g1": [ + "0x6123b6fea40de2a7e3595f9c35210da8a45a7e8c2f7da9eb4548e9210cfea81a", + "0x32a9b8347c512483812ee922dc75952842f8f3083edb6fe8d5c3c07e1340b683" + ], + "gamma_g2": "0xedf692d95cbdde46ddda5ef7d422436779445c5e66006a42761e1f12efde0018c212f3aeb785e49712e7a9353349aaf1255dfb31b7bf60723a480d9293938e19" + } +} \ No newline at end of file diff --git a/keyless/pepper/service/resources/keyless_config.json b/keyless/pepper/service/resources/keyless_config.json new file mode 100644 index 0000000000000..da097c690effe --- /dev/null +++ b/keyless/pepper/service/resources/keyless_config.json @@ -0,0 +1,17 @@ +{ + "type": "0x1::keyless_account::Configuration", + "data": { + "max_commited_epk_bytes": 93, + "max_exp_horizon_secs": "10000000", + "max_extra_field_bytes": 350, + "max_iss_val_bytes": 120, + "max_jwt_header_b64_bytes": 300, + "max_signatures_per_txn": 3, + "override_aud_vals": [], + "training_wheels_pubkey": { + "vec": [ + "0x5cd926a700e3997a3b319bfd003127ec7278eff14973c8cdcfbea54f3ea3669f" + ] + } + } +} diff --git a/keyless/pepper/service/src/groth16_vk.rs b/keyless/pepper/service/src/groth16_vk.rs new file mode 100644 index 0000000000000..d0be02a16c533 --- /dev/null +++ b/keyless/pepper/service/src/groth16_vk.rs @@ -0,0 +1,120 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::watcher::{unhexlify_api_bytes, ExternalResource}; +use anyhow::{anyhow, Result}; +use aptos_infallible::RwLock; +use ark_bn254::{Bn254, G1Affine, G2Affine}; +use ark_groth16::{PreparedVerifyingKey, VerifyingKey}; +use ark_serialize::CanonicalDeserialize; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +pub struct VKeyData { + pub alpha_g1: String, + pub beta_g2: String, + pub delta_g2: String, + pub gamma_abc_g1: Vec, + pub gamma_g2: String, +} + +/// On-chain representation of a VK. +/// +/// https://fullnode.testnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Groth16VerificationKey +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +pub struct OnChainGroth16VerificationKey { + /// Some type info returned by node API. + pub r#type: String, + pub data: VKeyData, +} + +impl OnChainGroth16VerificationKey { + pub fn to_ark_pvk(&self) -> Result> { + let mut gamma_abc_g1 = Vec::with_capacity(self.data.gamma_abc_g1.len()); + for (idx, onchain_ele) in self.data.gamma_abc_g1.iter().enumerate() { + let ark_ele = g1_from_api_repr(onchain_ele).map_err(|e| { + anyhow!("to_ark_pvk() failed with gamma_abc_g1[{idx}] convert err: {e}") + })?; + gamma_abc_g1.push(ark_ele); + } + let ark_vk = VerifyingKey { + alpha_g1: g1_from_api_repr(&self.data.alpha_g1) + .map_err(|e| anyhow!("to_ark_pvk() failed with alpha_g1 convert err: {e}"))?, + beta_g2: g2_from_api_repr(&self.data.beta_g2) + .map_err(|e| anyhow!("to_ark_pvk() failed with beta_g2 convert err: {e}"))?, + gamma_g2: g2_from_api_repr(&self.data.gamma_g2) + .map_err(|e| anyhow!("to_ark_pvk() failed with gamma_g2 convert err: {e}"))?, + delta_g2: g2_from_api_repr(&self.data.delta_g2) + .map_err(|e| anyhow!("to_ark_pvk() failed with delta_g2 convert err: {e}"))?, + gamma_abc_g1, + }; + Ok(PreparedVerifyingKey::from(ark_vk)) + } +} + +/// This variable holds the cached on-chain VK. A refresh loop exists to update it periodically. +pub static ONCHAIN_GROTH16_VK: Lazy>>> = + Lazy::new(|| Arc::new(RwLock::new(None))); + +fn g1_from_api_repr(api_repr: &str) -> Result { + let bytes = unhexlify_api_bytes(api_repr) + .map_err(|e| anyhow!("g1_from_api_repr() failed with unhex err: {e}"))?; + let ret = G1Affine::deserialize_compressed(bytes.as_slice()) + .map_err(|e| anyhow!("g1_from_api_repr() failed with g1 deser err: {e}"))?; + Ok(ret) +} + +fn g2_from_api_repr(api_repr: &str) -> Result { + let bytes = unhexlify_api_bytes(api_repr) + .map_err(|e| anyhow!("g2_from_api_repr() failed with unhex err: {e}"))?; + let ret = G2Affine::deserialize_compressed(bytes.as_slice()) + .map_err(|e| anyhow!("g2_from_api_repr() failed with g2 deser err: {e}"))?; + Ok(ret) +} + +impl ExternalResource for OnChainGroth16VerificationKey { + fn resource_name() -> String { + "OnChainGroth16VerificationKey".to_string() + } +} + +#[cfg(test)] +mod tests { + use crate::groth16_vk::{OnChainGroth16VerificationKey, VKeyData}; + use ark_bn254::{Bn254, Fr, G1Affine, G2Affine}; + use ark_groth16::Groth16; + use ark_serialize::CanonicalDeserialize; + + #[test] + fn test_to_ark_pvk() { + let api_repr = OnChainGroth16VerificationKey { + r#type: "0x1::keyless_account::Groth16VerificationKey".to_string(), + data: VKeyData { + alpha_g1: "0xe39cb24154872dbdbbdbc8056c6eb3e6cab3ad82f80ded72ed4c9301c5b3da15".to_string(), + beta_g2: "0x9a732e38644f89ad2c7bd629b84d6b81f2e83ca4b3cddfd99c0254e49332861e2fcec4f74545abdd42c8857ff8df6d3f6b3670f930d1d5ba961655ea38ded315".to_string(), + delta_g2: "0x04c7b3a2734731369a281424c2bd7af229b92496527fd0a01bfe4a5c01e0a92f256921817b6d6cf040ccd483d81738ac88571b57009f182946e8a88cced03a01".to_string(), + gamma_abc_g1: vec![ + "0x2f4f4bc4acbea0c3bae9e676fb59537e2e46994d5896e286e6fcccc7e14b1b2d".to_string(), + "0x979308443fbac05f6d22a16525c26246e965a9be68e163154f44b20d6b2ddf18".to_string(), + ], + gamma_g2: "0xedf692d95cbdde46ddda5ef7d422436779445c5e66006a42761e1f12efde0018c212f3aeb785e49712e7a9353349aaf1255dfb31b7bf60723a480d9293938e19".to_string(), + }, + }; + + let ark_pvk = api_repr.to_ark_pvk().unwrap(); + let proof = ark_groth16::Proof { + a: G1Affine::deserialize_compressed(hex::decode("fd6ae6c19f7eb7362e420e3e359f3d85c0030b779a815627b805276e45018817").unwrap().as_slice()).unwrap(), + b: G2Affine::deserialize_compressed(hex::decode("c4af5f1e793653d80009c19637b326f78a46d9d031e9e36a6f87e296ba04e31322af2d8d5c3d4129b6b2f3d222f741ce17145ab62f1b238f3f9e8c88831da297").unwrap().as_slice()).unwrap(), + c: G1Affine::deserialize_compressed(hex::decode("a856a923cde90ecb6f8955c9627ede579e67b7082431b965d011fca578892096").unwrap().as_slice()).unwrap(), + }; + let public_inputs = vec![Fr::deserialize_compressed( + hex::decode("08aba90163b227d54013b7d1a892b20edf149a23acf810acf78e8baf8e770d11") + .unwrap() + .as_slice(), + ) + .unwrap()]; + assert!(Groth16::::verify_proof(&ark_pvk, &proof, &public_inputs).unwrap()); + } +} diff --git a/keyless/pepper/service/src/keyless_config.rs b/keyless/pepper/service/src/keyless_config.rs new file mode 100644 index 0000000000000..a70b6762dee5b --- /dev/null +++ b/keyless/pepper/service/src/keyless_config.rs @@ -0,0 +1,69 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::watcher::{unhexlify_api_bytes, ExternalResource}; +use anyhow::{anyhow, Result}; +use aptos_infallible::RwLock; +use aptos_types::keyless::Configuration; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +pub struct TrainingWheelsPubKey { + vec: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +pub struct OnChainKeylessConfiguration { + /// Some type info returned by node API. + pub r#type: String, + pub data: ConfigData, +} + +impl OnChainKeylessConfiguration { + pub fn to_rust_repr(&self) -> Result { + let training_wheels_pubkey = self + .data + .training_wheels_pubkey + .vec + .first() + .map(|v| unhexlify_api_bytes(v.as_str())) + .transpose() + .map_err(|e| anyhow!("to_rust_repr() failed with unhexlify err: {e}"))?; + let ret = Configuration { + override_aud_vals: self.data.override_aud_vals.clone(), + max_signatures_per_txn: self.data.max_signatures_per_txn, + max_exp_horizon_secs: self.data.max_exp_horizon_secs.parse().map_err(|e| { + anyhow!("to_rust_repr() failed at max_exp_horizon_secs convert: {e}") + })?, + training_wheels_pubkey, + max_commited_epk_bytes: self.data.max_commited_epk_bytes, + max_iss_val_bytes: self.data.max_iss_val_bytes, + max_extra_field_bytes: self.data.max_extra_field_bytes, + max_jwt_header_b64_bytes: self.data.max_jwt_header_b64_bytes, + }; + Ok(ret) + } +} + +impl ExternalResource for OnChainKeylessConfiguration { + fn resource_name() -> String { + "OnChainKeylessConfiguration".to_string() + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +pub struct ConfigData { + pub max_commited_epk_bytes: u16, + pub max_exp_horizon_secs: String, + pub max_extra_field_bytes: u16, + pub max_iss_val_bytes: u16, + pub max_jwt_header_b64_bytes: u32, + pub max_signatures_per_txn: u16, + pub override_aud_vals: Vec, + pub training_wheels_pubkey: TrainingWheelsPubKey, +} + +pub static ONCHAIN_KEYLESS_CONFIG: Lazy>>> = + Lazy::new(|| Arc::new(RwLock::new(None))); diff --git a/keyless/pepper/service/src/lib.rs b/keyless/pepper/service/src/lib.rs index f231a7fd4d8f7..3c6fe36e779fc 100644 --- a/keyless/pepper/service/src/lib.rs +++ b/keyless/pepper/service/src/lib.rs @@ -4,6 +4,8 @@ use crate::{ account_db::{init_account_db, ACCOUNT_RECOVERY_DB}, account_managers::ACCOUNT_MANAGERS, + groth16_vk::ONCHAIN_GROTH16_VK, + keyless_config::ONCHAIN_KEYLESS_CONFIG, vuf_keys::VUF_SK, ProcessingFailure::{BadRequest, InternalError}, }; @@ -30,7 +32,7 @@ use aptos_types::{ account_address::AccountAddress, keyless::{ get_public_inputs_hash, Configuration, EphemeralCertificate, Groth16ProofAndStatement, - IdCommitment, KeylessPublicKey, KeylessSignature, OpenIdSig, DEVNET_VERIFICATION_KEY, ZKP, + IdCommitment, KeylessPublicKey, KeylessSignature, OpenIdSig, ZKP, }, transaction::authenticator::{ AnyPublicKey, AnySignature, AuthenticationKey, EphemeralPublicKey, @@ -47,9 +49,12 @@ use uuid::Uuid; pub mod about; pub mod account_db; pub mod account_managers; +pub mod groth16_vk; pub mod jwk; +pub mod keyless_config; pub mod metrics; pub mod vuf_keys; +pub mod watcher; pub type Issuer = String; pub type KeyID = String; @@ -182,7 +187,13 @@ impl HandlerTrait for V0VerifyHandler { .map_err(|e| BadRequest(format!("JWT header decoding error: {e}")))?; let jwk = jwk::cached_decoding_key_as_rsa(iss_val, &jwt_header.kid) .map_err(|e| BadRequest(format!("JWK not found: {e}")))?; - let config = Configuration::new_for_devnet(); + let config_api_repr = + { ONCHAIN_KEYLESS_CONFIG.read().as_ref().cloned() }.ok_or_else(|| { + InternalError("API keyless config not cached locally.".to_string()) + })?; + let config = config_api_repr + .to_rust_repr() + .map_err(|e| InternalError(format!("Could not parse API keyless config: {e}")))?; let training_wheels_pk = match &config.training_wheels_pubkey { None => None, // This takes ~4.4 microseconds, so we are not too concerned about speed here. @@ -247,8 +258,15 @@ impl HandlerTrait for V0VerifyHandler { } } - let result = zksig - .verify_groth16_proof(public_inputs_hash, &DEVNET_VERIFICATION_KEY); + let onchain_groth16_vk = + { ONCHAIN_GROTH16_VK.read().as_ref().cloned() }.ok_or_else( + || InternalError("No Groth16 VK cached locally.".to_string()), + )?; + let ark_groth16_pvk = onchain_groth16_vk.to_ark_pvk().map_err(|e| { + InternalError(format!("Onchain-to-ark convertion err: {e}")) + })?; + let result = + zksig.verify_groth16_proof(public_inputs_hash, &ark_groth16_pvk); result.map_err(|_| { // println!("[aptos-vm][groth16] ZKP verification failed"); // println!("[aptos-vm][groth16] PIH: {}", public_inputs_hash); diff --git a/keyless/pepper/service/src/main.rs b/keyless/pepper/service/src/main.rs index cabbd3e0262f2..6a722b3fa954e 100644 --- a/keyless/pepper/service/src/main.rs +++ b/keyless/pepper/service/src/main.rs @@ -6,9 +6,12 @@ use aptos_keyless_pepper_service::{ about::ABOUT_JSON, account_db::{init_account_db, ACCOUNT_RECOVERY_DB}, account_managers::ACCOUNT_MANAGERS, + groth16_vk::ONCHAIN_GROTH16_VK, jwk::{self, parse_jwks, DECODING_KEY_CACHE}, + keyless_config::ONCHAIN_KEYLESS_CONFIG, metrics::start_metric_server, vuf_keys::{PEPPER_VUF_VERIFICATION_KEY_JSON, VUF_SK}, + watcher::start_external_resource_refresh_loop, HandlerTrait, ProcessingFailure::{BadRequest, InternalError}, V0FetchHandler, V0SignatureHandler, V0VerifyHandler, @@ -37,6 +40,14 @@ async fn handle_request(req: Request) -> Result, Infallible (&Method::GET, "/about") => { build_response(origin, StatusCode::OK, ABOUT_JSON.deref().clone()) }, + (&Method::GET, "/cached/keyless-config") => build_response_for_optional_resource( + origin, + ONCHAIN_KEYLESS_CONFIG.read().as_ref().cloned(), + ), + (&Method::GET, "/cached/groth16-vk") => build_response_for_optional_resource( + origin, + ONCHAIN_GROTH16_VK.read().as_ref().cloned(), + ), (&Method::GET, "/v0/vuf-pub-key") => build_response( origin, StatusCode::OK, @@ -74,6 +85,20 @@ async fn main() { } aptos_logger::Logger::new().init(); start_metric_server(); + if let Ok(url) = std::env::var("ONCHAIN_GROTH16_VK_URL") { + start_external_resource_refresh_loop( + &url, + Duration::from_secs(10), + ONCHAIN_GROTH16_VK.clone(), + ); + } + if let Ok(url) = std::env::var("ONCHAIN_KEYLESS_CONFIG_URL") { + start_external_resource_refresh_loop( + &url, + Duration::from_secs(10), + ONCHAIN_KEYLESS_CONFIG.clone(), + ); + } // TODO: JWKs should be from on-chain states? jwk::start_jwk_refresh_loop( @@ -172,3 +197,16 @@ fn build_response(origin: String, status_code: StatusCode, body_str: String) -> .body(Body::from(body_str)) .expect("Response should build") } + +fn build_response_for_optional_resource( + origin: String, + res: Option, +) -> Response { + match res { + None => build_response(origin, StatusCode::NOT_FOUND, "".to_string()), + Some(val) => match serde_json::to_string(&val) { + Ok(s) => build_response(origin, StatusCode::OK, s), + Err(e) => build_response(origin, StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + }, + } +} diff --git a/keyless/pepper/service/src/watcher.rs b/keyless/pepper/service/src/watcher.rs new file mode 100644 index 0000000000000..72738e602c371 --- /dev/null +++ b/keyless/pepper/service/src/watcher.rs @@ -0,0 +1,77 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{anyhow, ensure, Result}; +use aptos_infallible::RwLock; +use aptos_logger::{debug, info, warn}; +use serde::de::DeserializeOwned; +use std::{sync::Arc, time::Duration}; + +pub async fn fetch_and_cache_resource( + resource_url: &str, + resource_holder: &RwLock>, +) -> Result<()> { + let resource = reqwest::get(resource_url).await?.json::().await?; + *resource_holder.write() = Some(resource); + Ok(()) +} + +pub fn start_external_resource_refresh_loop< + T: DeserializeOwned + ExternalResource + Send + Sync + 'static, +>( + url: &str, + refresh_interval: Duration, + local_cache: Arc>>, +) { + info!( + "Starting external resource refresh loop for {}", + T::resource_name() + ); + let url = url.to_string(); + let _handle = tokio::spawn(async move { + loop { + let result = fetch_and_cache_resource(&url, local_cache.as_ref()).await; + match result { + Ok(_vk) => { + debug!("fetch_and_cache_resource {} succeeded.", T::resource_name()); + }, + Err(e) => { + warn!( + "fetch_and_cache_resource {} failed: {}", + T::resource_name(), + e + ); + }, + } + + tokio::time::sleep(refresh_interval).await; + } + }); +} + +pub trait ExternalResource { + fn resource_name() -> String; +} + +pub fn unhexlify_api_bytes(api_output: &str) -> Result> { + ensure!(api_output.len() >= 2); + let lower = api_output.to_lowercase(); + ensure!(&lower[0..2] == "0x"); + let bytes = hex::decode(&lower[2..]) + .map_err(|e| anyhow!("unhexlify_api_bytes() failed at decoding: {e}"))?; + Ok(bytes) +} + +#[test] +fn test_unhexlify_api_bytes() { + assert_eq!( + vec![0x00_u8, 0x01, 0xFF], + unhexlify_api_bytes("0x0001ff").unwrap() + ); + assert!(unhexlify_api_bytes("0x").unwrap().is_empty()); + assert!(unhexlify_api_bytes("0001ff").is_err()); + assert!(unhexlify_api_bytes("0x0001fg").is_err()); + assert!(unhexlify_api_bytes("000").is_err()); + assert!(unhexlify_api_bytes("0").is_err()); + assert!(unhexlify_api_bytes("").is_err()); +} diff --git a/testsuite/smoke-test/src/keyless.rs b/testsuite/smoke-test/src/keyless.rs index 612ef3a556cf9..22c1457a47af9 100644 --- a/testsuite/smoke-test/src/keyless.rs +++ b/testsuite/smoke-test/src/keyless.rs @@ -31,7 +31,7 @@ use aptos_types::{ }, AnyKeylessPublicKey, Configuration, EphemeralCertificate, Groth16ProofAndStatement, Groth16VerificationKey, KeylessPublicKey, KeylessSignature, TransactionAndProof, - DEVNET_VERIFICATION_KEY, KEYLESS_ACCOUNT_MODULE_NAME, + KEYLESS_ACCOUNT_MODULE_NAME, VERIFICATION_KEY_FOR_TESTING, }, on_chain_config::{FeatureFlag, Features}, transaction::{ @@ -891,6 +891,25 @@ pub(crate) async fn spawn_network_and_execute_gov_proposals( .await .expect("Epoch 2 taking too long to come!"); + let vk_for_testing = Groth16VerificationKey::from(VERIFICATION_KEY_FOR_TESTING.clone()); + let script = get_rotate_vk_governance_script(&vk_for_testing); + + let gas_options = GasOptions { + gas_unit_price: Some(100), + max_gas: Some(2000000), + expiration_secs: 60, + }; + let txn_summary = cli + .run_script_with_gas_options(root_idx, &script, Some(gas_options.clone())) + .await + .unwrap(); + debug!("txn_summary={:?}", txn_summary); + + let mut info = swarm.aptos_public_info(); + + // Increment sequence number since we installed the VK + info.root_account().increment_sequence_number(); + let vk = print_account_resource::( &client, AccountAddress::ONE, @@ -900,10 +919,7 @@ pub(crate) async fn spawn_network_and_execute_gov_proposals( ) .await; - assert_eq!( - vk, - Groth16VerificationKey::from(DEVNET_VERIFICATION_KEY.clone()) - ); + assert_eq!(vk, vk_for_testing); let old_config = print_account_resource::( &client, @@ -962,11 +978,6 @@ fun main(core_resources: &signer) {{ hex::encode(training_wheels_pk.to_bytes()) ); - let gas_options = GasOptions { - gas_unit_price: Some(100), - max_gas: Some(2000000), - expiration_secs: 60, - }; let txn_summary = cli .run_script_with_gas_options(root_idx, &script, Some(gas_options)) .await @@ -998,8 +1009,6 @@ fun main(core_resources: &signer) {{ assert_ne!(old_config, new_config); assert_eq!(new_config.max_exp_horizon_secs, max_exp_horizon_secs); - let mut info = swarm.aptos_public_info(); - // Increment sequence number since we patched a JWK info.root_account().increment_sequence_number(); @@ -1014,12 +1023,7 @@ async fn get_latest_jwkset(rest_client: &Client) -> PatchedJWKs { response.into_inner() } -async fn rotate_vk_by_governance<'a>( - cli: &mut CliTestFramework, - info: &mut AptosPublicInfo, - vk: &Groth16VerificationKey, - root_idx: usize, -) { +fn get_rotate_vk_governance_script(vk: &Groth16VerificationKey) -> String { let script = format!( r#" script {{ @@ -1045,6 +1049,17 @@ script {{ ); debug!("Move script for changing VK follows below:\n{:?}", script); + script +} + +async fn rotate_vk_by_governance<'a>( + cli: &mut CliTestFramework, + info: &mut AptosPublicInfo, + vk: &Groth16VerificationKey, + root_idx: usize, +) { + let script = get_rotate_vk_governance_script(vk); + print_account_resource::( info.client(), AccountAddress::ONE, diff --git a/types/src/keyless/bn254_circom.rs b/types/src/keyless/bn254_circom.rs index 1991e32c91420..546bdc968ef29 100644 --- a/types/src/keyless/bn254_circom.rs +++ b/types/src/keyless/bn254_circom.rs @@ -399,7 +399,7 @@ mod test { G1Bytes, G2Bytes, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, G2_PROJECTIVE_COMPRESSED_NUM_BYTES, }, - circuit_constants::devnet_prepared_vk, + circuit_constants::prepared_vk_for_testing, Groth16VerificationKey, }; use ark_bn254::Bn254; @@ -436,7 +436,7 @@ mod test { // Tests conversion between the devnet ark_groth16::PreparedVerificationKey and our Move // representation of it. fn print_groth16_pvk() { - let groth16_vk: Groth16VerificationKey = devnet_prepared_vk().into(); + let groth16_vk: Groth16VerificationKey = prepared_vk_for_testing().into(); println!("alpha_g1: {:?}", hex::encode(groth16_vk.alpha_g1.clone())); println!("beta_g2: {:?}", hex::encode(groth16_vk.beta_g2.clone())); @@ -448,6 +448,6 @@ mod test { let same_pvk: PreparedVerifyingKey = groth16_vk.try_into().unwrap(); - assert_eq!(same_pvk, devnet_prepared_vk()); + assert_eq!(same_pvk, prepared_vk_for_testing()); } } diff --git a/types/src/keyless/circuit_constants.rs b/types/src/keyless/circuit_constants.rs index ce0017e1fe5a9..7013b1fb3fbb6 100644 --- a/types/src/keyless/circuit_constants.rs +++ b/types/src/keyless/circuit_constants.rs @@ -27,7 +27,7 @@ pub(crate) const MAX_COMMITED_EPK_BYTES: u16 = /// This function uses the decimal uncompressed point serialization which is outputted by circom. /// https://github.com/aptos-labs/devnet-groth16-keys/commit/02e5675f46ce97f8b61a4638e7a0aaeaa4351f76 -pub fn devnet_prepared_vk() -> PreparedVerifyingKey { +pub fn prepared_vk_for_testing() -> PreparedVerifyingKey { // Convert the projective points to affine. let alpha_g1 = g1_projective_str_to_affine( "20491192805390485299153009773594534940189261866228447918068658471970481763042", diff --git a/types/src/keyless/mod.rs b/types/src/keyless/mod.rs index 39b546673a723..e686bfc7c6644 100644 --- a/types/src/keyless/mod.rs +++ b/types/src/keyless/mod.rs @@ -32,7 +32,7 @@ pub mod proof_simulation; pub mod test_utils; mod zkp_sig; -use crate::keyless::circuit_constants::devnet_prepared_vk; +use crate::keyless::circuit_constants::prepared_vk_for_testing; pub use bn254_circom::{ g1_projective_str_to_affine, g2_projective_str_to_affine, get_public_inputs_hash, G1Bytes, G2Bytes, G1_PROJECTIVE_COMPRESSED_NUM_BYTES, G2_PROJECTIVE_COMPRESSED_NUM_BYTES, @@ -47,9 +47,9 @@ pub use zkp_sig::ZKP; /// The name of the Move module for keyless accounts deployed at 0x1. pub const KEYLESS_ACCOUNT_MODULE_NAME: &str = "keyless_account"; -/// The devnet VK that is initialized during genesis. -pub static DEVNET_VERIFICATION_KEY: Lazy> = - Lazy::new(devnet_prepared_vk); +/// A VK that we use often for keyless e2e tests and smoke tests. +pub static VERIFICATION_KEY_FOR_TESTING: Lazy> = + Lazy::new(prepared_vk_for_testing); #[macro_export] macro_rules! invalid_signature { diff --git a/types/src/keyless/test_utils.rs b/types/src/keyless/test_utils.rs index 1d275ff3c8b30..83a0021f864a8 100644 --- a/types/src/keyless/test_utils.rs +++ b/types/src/keyless/test_utils.rs @@ -403,7 +403,7 @@ mod test { get_sample_epk_blinder, get_sample_esk, get_sample_exp_date, get_sample_groth16_sig_and_pk, get_sample_jwt_token, get_sample_pepper, }, - Configuration, Groth16Proof, OpenIdSig, DEVNET_VERIFICATION_KEY, + Configuration, Groth16Proof, OpenIdSig, VERIFICATION_KEY_FOR_TESTING, }, transaction::authenticator::EphemeralPublicKey, }; @@ -521,7 +521,7 @@ mod test { // Verify the proof with the test verifying key. If this fails the verifying key does not match the proving used // to generate the proof. proof - .verify_proof(public_inputs_hash, DEVNET_VERIFICATION_KEY.deref()) + .verify_proof(public_inputs_hash, VERIFICATION_KEY_FOR_TESTING.deref()) .unwrap(); prover_response diff --git a/types/src/keyless/tests.rs b/types/src/keyless/tests.rs index 52293caf94336..95d1ad41de51c 100644 --- a/types/src/keyless/tests.rs +++ b/types/src/keyless/tests.rs @@ -9,7 +9,7 @@ use crate::keyless::{ get_sample_openid_sig_and_pk, }, Configuration, EphemeralCertificate, KeylessPublicKey, KeylessSignature, - DEVNET_VERIFICATION_KEY, + VERIFICATION_KEY_FOR_TESTING, }; use aptos_crypto::poseidon_bn254::keyless::fr_to_bytes_le; use std::ops::{AddAssign, Deref}; @@ -74,7 +74,7 @@ fn test_keyless_groth16_proof_verification() { ); proof - .verify_groth16_proof(public_inputs_hash, DEVNET_VERIFICATION_KEY.deref()) + .verify_groth16_proof(public_inputs_hash, VERIFICATION_KEY_FOR_TESTING.deref()) .unwrap(); }