Skip to content

Commit

Permalink
feat(optimism): Add secp256r1 precompile for Fjord (#1436)
Browse files Browse the repository at this point in the history
* feat(optimism): Add secp256r1 precompile for Fjord

* Fix docs

* Fix nostd build

* Load fjord precompiles via optimism handler register

* Remove outdated fjord() precompile spec constructor

* Document the secp256r1 feature

* Address feedback

* Handle invalid signatures

* Update crates/precompile/src/secp256r1.rs

* Update crates/precompile/src/secp256r1.rs

* Blank return on failed signature verification

* Add test case for invalid (zero) pubkey

---------

Co-authored-by: rakita <[email protected]>
  • Loading branch information
BrianBland and rakita authored May 26, 2024
1 parent 2ce53cd commit 928883c
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 7 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion crates/precompile/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ secp256k1 = { version = "0.29.0", default-features = false, features = [
# BLS12-381 precompiles
blst = { version = "0.3.11", optional = true }

# p256verify precompile
p256 = { version = "0.13.2", optional = true, default-features = false, features = ["ecdsa"] }

[dev-dependencies]
criterion = { version = "0.5" }
rand = { version = "0.8", features = ["std"] }
Expand All @@ -67,7 +70,7 @@ std = [
hashbrown = ["revm-primitives/hashbrown"]
asm-keccak = ["revm-primitives/asm-keccak"]

optimism = ["revm-primitives/optimism"]
optimism = ["revm-primitives/optimism", "secp256r1"]
# Optimism default handler enabled Optimism handler register by default in EvmBuilder.
optimism-default-handler = [
"optimism",
Expand All @@ -77,6 +80,9 @@ negate-optimism-default-handler = [
"revm-primitives/negate-optimism-default-handler",
]

# Enables the p256verify precompile.
secp256r1 = ["dep:p256"]

# These libraries may not work on all no_std platforms as they depend on C.

# Enables the KZG point evaluation precompile.
Expand Down
4 changes: 3 additions & 1 deletion crates/precompile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ pub mod identity;
pub mod kzg_point_evaluation;
pub mod modexp;
pub mod secp256k1;
#[cfg(feature = "secp256r1")]
pub mod secp256r1;
pub mod utilities;

use core::hash::Hash;
Expand Down Expand Up @@ -271,7 +273,7 @@ impl PrecompileSpecId {
#[cfg(feature = "optimism")]
BEDROCK | REGOLITH | CANYON => Self::BERLIN,
#[cfg(feature = "optimism")]
ECOTONE => Self::CANCUN,
ECOTONE | FJORD => Self::CANCUN,
}
}
}
Expand Down
128 changes: 128 additions & 0 deletions crates/precompile/src/secp256r1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! # EIP-7212 secp256r1 Precompile
//!
//! This module implements the [EIP-7212](https://eips.ethereum.org/EIPS/eip-7212) precompile for
//! secp256r1 curve support.
//!
//! The main purpose of this precompile is to verify ECDSA signatures that use the secp256r1, or
//! P256 elliptic curve. The [`P256VERIFY`] const represents the implementation of this precompile,
//! with the address that it is currently deployed at.
use crate::{u64_to_address, Precompile, PrecompileWithAddress};
use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
use revm_primitives::{Bytes, PrecompileError, PrecompileResult, B256};

/// Base gas fee for secp256r1 p256verify operation.
const P256VERIFY_BASE: u64 = 3450;

/// Returns the secp256r1 precompile with its address.
pub fn precompiles() -> impl Iterator<Item = PrecompileWithAddress> {
[P256VERIFY].into_iter()
}

/// [EIP-7212](https://eips.ethereum.org/EIPS/eip-7212#specification) secp256r1 precompile.
pub const P256VERIFY: PrecompileWithAddress =
PrecompileWithAddress(u64_to_address(0x100), Precompile::Standard(p256_verify));

/// secp256r1 precompile logic. It takes the input bytes sent to the precompile
/// and the gas limit. The output represents the result of verifying the
/// secp256r1 signature of the input.
///
/// The input is encoded as follows:
///
/// | signed message hash | r | s | public key x | public key y |
/// | :-----------------: | :-: | :-: | :----------: | :----------: |
/// | 32 | 32 | 32 | 32 | 32 |
pub fn p256_verify(input: &Bytes, gas_limit: u64) -> PrecompileResult {
if P256VERIFY_BASE > gas_limit {
return Err(PrecompileError::OutOfGas);
}
let result = if verify_impl(input).is_some() {
B256::with_last_byte(1).into()
} else {
Bytes::new()
};
Ok((P256VERIFY_BASE, result))
}

/// Returns `Some(())` if the signature included in the input byte slice is
/// valid, `None` otherwise.
pub fn verify_impl(input: &[u8]) -> Option<()> {
if input.len() != 160 {
return None;
}

// msg signed (msg is already the hash of the original message)
let msg = &input[..32];
// r, s: signature
let sig = &input[32..96];
// x, y: public key
let pk = &input[96..160];

// prepend 0x04 to the public key: uncompressed form
let mut uncompressed_pk = [0u8; 65];
uncompressed_pk[0] = 0x04;
uncompressed_pk[1..].copy_from_slice(pk);

// Can fail only if the input is not exact length.
let signature = Signature::from_slice(sig).ok()?;
// Can fail if the input is not valid, so we have to propagate the error.
let public_key = VerifyingKey::from_sec1_bytes(&uncompressed_pk).ok()?;

public_key.verify_prehash(msg, &signature).ok()
}

#[cfg(test)]
mod test {
use super::*;
use revm_primitives::hex::FromHex;
use rstest::rstest;

#[rstest]
// test vectors from https://github.com/daimo-eth/p256-verifier/tree/master/test-vectors
#[case::ok_1("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e", true)]
#[case::ok_2("3fec5769b5cf4e310a7d150508e82fb8e3eda1c2c94c61492d3bd8aea99e06c9e22466e928fdccef0de49e3503d2657d00494a00e764fd437bdafa05f5922b1fbbb77c6817ccf50748419477e843d5bac67e6a70e97dde5a57e0c983b777e1ad31a80482dadf89de6302b1988c82c29544c9c07bb910596158f6062517eb089a2f54c9a0f348752950094d3228d3b940258c75fe2a413cb70baa21dc2e352fc5", true)]
#[case::ok_3("e775723953ead4a90411a02908fd1a629db584bc600664c609061f221ef6bf7c440066c8626b49daaa7bf2bcc0b74be4f7a1e3dcf0e869f1542fe821498cbf2de73ad398194129f635de4424a07ca715838aefe8fe69d1a391cfa70470795a80dd056866e6e1125aff94413921880c437c9e2570a28ced7267c8beef7e9b2d8d1547d76dfcf4bee592f5fefe10ddfb6aeb0991c5b9dbbee6ec80d11b17c0eb1a", true)]
#[case::ok_4("b5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdcef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", true)]
#[case::ok_5("858b991cfd78f16537fe6d1f4afd10273384db08bdfc843562a22b0626766686f6aec8247599f40bfe01bec0e0ecf17b4319559022d4d9bf007fe929943004eb4866760dedf31b7c691f5ce665f8aae0bda895c23595c834fecc2390a5bcc203b04afcacbb4280713287a2d0c37e23f7513fab898f2c1fefa00ec09a924c335d9b629f1d4fb71901c3e59611afbfea354d101324e894c788d1c01f00b3c251b2", true)]
#[case::fail_wrong_msg_1("3cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e", false)]
#[case::fail_wrong_msg_2("afec5769b5cf4e310a7d150508e82fb8e3eda1c2c94c61492d3bd8aea99e06c9e22466e928fdccef0de49e3503d2657d00494a00e764fd437bdafa05f5922b1fbbb77c6817ccf50748419477e843d5bac67e6a70e97dde5a57e0c983b777e1ad31a80482dadf89de6302b1988c82c29544c9c07bb910596158f6062517eb089a2f54c9a0f348752950094d3228d3b940258c75fe2a413cb70baa21dc2e352fc5", false)]
#[case::fail_wrong_msg_3("f775723953ead4a90411a02908fd1a629db584bc600664c609061f221ef6bf7c440066c8626b49daaa7bf2bcc0b74be4f7a1e3dcf0e869f1542fe821498cbf2de73ad398194129f635de4424a07ca715838aefe8fe69d1a391cfa70470795a80dd056866e6e1125aff94413921880c437c9e2570a28ced7267c8beef7e9b2d8d1547d76dfcf4bee592f5fefe10ddfb6aeb0991c5b9dbbee6ec80d11b17c0eb1a", false)]
#[case::fail_wrong_msg_4("c5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdcef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", false)]
#[case::fail_wrong_msg_5("958b991cfd78f16537fe6d1f4afd10273384db08bdfc843562a22b0626766686f6aec8247599f40bfe01bec0e0ecf17b4319559022d4d9bf007fe929943004eb4866760dedf31b7c691f5ce665f8aae0bda895c23595c834fecc2390a5bcc203b04afcacbb4280713287a2d0c37e23f7513fab898f2c1fefa00ec09a924c335d9b629f1d4fb71901c3e59611afbfea354d101324e894c788d1c01f00b3c251b2", false)]
#[case::fail_short_input_1("4cee90eb86eaa050036147a12d49004b6a", false)]
#[case::fail_short_input_2("4cee90eb86eaa050036147a12d49004b6a958b991cfd78f16537fe6d1f4afd10273384db08bdfc843562a22b0626766686f6aec8247599f40bfe01bec0e0ecf17b4319559022d4d9bf007fe929943004eb4866760dedf319", false)]
#[case::fail_long_input("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e00", false)]
#[case::fail_invalid_sig("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e", false)]
#[case::fail_invalid_pubkey("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false)]
fn test_sig_verify(#[case] input: &str, #[case] expect_success: bool) {
let input = Bytes::from_hex(input).unwrap();
let target_gas = 3_500u64;
let (gas_used, res) = p256_verify(&input, target_gas).unwrap();
assert_eq!(gas_used, 3_450u64);
let expected_result = if expect_success {
B256::with_last_byte(1).into()
} else {
Bytes::new()
};
assert_eq!(res, expected_result);
}

#[rstest]
fn test_not_enough_gas_errors() {
let input = Bytes::from_hex("4cee90eb86eaa050036147a12d49004b6b9c72bd725d39d4785011fe190f0b4da73bd4903f0ce3b639bbbf6e8e80d16931ff4bcf5993d58468e8fb19086e8cac36dbcd03009df8c59286b162af3bd7fcc0450c9aa81be5d10d312af6c66b1d604aebd3099c618202fcfe16ae7770b0c49ab5eadf74b754204a3bb6060e44eff37618b065f9832de4ca6ca971a7a1adc826d0f7c00181a5fb2ddf79ae00b4e10e").unwrap();
let target_gas = 2_500u64;
let result = p256_verify(&input, target_gas);

assert!(result.is_err());
assert_eq!(result.err(), Some(PrecompileError::OutOfGas));
}

#[rstest]
#[case::ok_1("b5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2da3a81046703fccf468b48b145f939efdbb96c3786db712b3113bb2488ef286cdcef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", true)]
#[case::fail_1("b5a77e7a90aa14e0bf5f337f06f597148676424fae26e175c6e5621c34351955289f319789da424845c9eac935245fcddd805950e2f02506d09be7e411199556d262144475b1fa46ad85250728c600c53dfd10f8b3f4adf140e27241aec3c2daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaef8afe82d200a5bb36b5462166e8ce77f2d831a52ef2135b2af188110beaefb1", false)]
fn test_verify_impl(#[case] input: &str, #[case] expect_success: bool) {
let input = Bytes::from_hex(input).unwrap();
let result = verify_impl(&input);

assert_eq!(result.is_some(), expect_success);
}
}
43 changes: 42 additions & 1 deletion crates/primitives/src/specification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ pub enum SpecId {
CANYON = 19,
CANCUN = 20,
ECOTONE = 21,
PRAGUE = 22,
FJORD = 22,
PRAGUE = 23,
#[default]
LATEST = u8::MAX,
}
Expand Down Expand Up @@ -114,6 +115,8 @@ impl From<&str> for SpecId {
"Canyon" => SpecId::CANYON,
#[cfg(feature = "optimism")]
"Ecotone" => SpecId::ECOTONE,
#[cfg(feature = "optimism")]
"Fjord" => SpecId::FJORD,
_ => Self::LATEST,
}
}
Expand Down Expand Up @@ -149,6 +152,8 @@ impl From<SpecId> for &'static str {
SpecId::CANYON => "Canyon",
#[cfg(feature = "optimism")]
SpecId::ECOTONE => "Ecotone",
#[cfg(feature = "optimism")]
SpecId::FJORD => "Fjord",
SpecId::LATEST => "Latest",
}
}
Expand Down Expand Up @@ -207,6 +212,8 @@ spec!(REGOLITH, RegolithSpec);
spec!(CANYON, CanyonSpec);
#[cfg(feature = "optimism")]
spec!(ECOTONE, EcotoneSpec);
#[cfg(feature = "optimism")]
spec!(FJORD, FjordSpec);

#[cfg(not(feature = "optimism"))]
#[macro_export]
Expand Down Expand Up @@ -354,6 +361,10 @@ macro_rules! spec_to_generic {
use $crate::EcotoneSpec as SPEC;
$e
}
$crate::SpecId::FJORD => {
use $crate::FjordSpec as SPEC;
$e
}
}
}};
}
Expand Down Expand Up @@ -390,6 +401,10 @@ mod tests {
#[cfg(feature = "optimism")]
spec_to_generic!(CANYON, assert_eq!(SPEC::SPEC_ID, CANYON));
spec_to_generic!(CANCUN, assert_eq!(SPEC::SPEC_ID, CANCUN));
#[cfg(feature = "optimism")]
spec_to_generic!(ECOTONE, assert_eq!(SPEC::SPEC_ID, ECOTONE));
#[cfg(feature = "optimism")]
spec_to_generic!(FJORD, assert_eq!(SPEC::SPEC_ID, FJORD));
spec_to_generic!(PRAGUE, assert_eq!(SPEC::SPEC_ID, PRAGUE));
spec_to_generic!(LATEST, assert_eq!(SPEC::SPEC_ID, LATEST));
}
Expand Down Expand Up @@ -485,4 +500,30 @@ mod optimism_tests {
assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::CANYON));
assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::ECOTONE));
}

#[test]
fn test_fjord_post_merge_hardforks() {
assert!(FjordSpec::enabled(SpecId::MERGE));
assert!(FjordSpec::enabled(SpecId::SHANGHAI));
assert!(FjordSpec::enabled(SpecId::CANCUN));
assert!(!FjordSpec::enabled(SpecId::LATEST));
assert!(FjordSpec::enabled(SpecId::BEDROCK));
assert!(FjordSpec::enabled(SpecId::REGOLITH));
assert!(FjordSpec::enabled(SpecId::CANYON));
assert!(FjordSpec::enabled(SpecId::ECOTONE));
assert!(FjordSpec::enabled(SpecId::FJORD));
}

#[test]
fn test_fjord_post_merge_hardforks_spec_id() {
assert!(SpecId::enabled(SpecId::FJORD, SpecId::MERGE));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::SHANGHAI));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::CANCUN));
assert!(!SpecId::enabled(SpecId::FJORD, SpecId::LATEST));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::BEDROCK));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::REGOLITH));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::CANYON));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::ECOTONE));
assert!(SpecId::enabled(SpecId::FJORD, SpecId::FJORD));
}
}
2 changes: 1 addition & 1 deletion crates/revm/src/db/in_memory_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ mod tests {
let serialized = serde_json::to_string(&init_state).unwrap();
let deserialized: CacheDB<EmptyDB> = serde_json::from_str(&serialized).unwrap();

assert!(deserialized.accounts.get(&account).is_some());
assert!(deserialized.accounts.contains_key(&account));
assert_eq!(
deserialized.accounts.get(&account).unwrap().info.nonce,
nonce
Expand Down
4 changes: 2 additions & 2 deletions crates/revm/src/optimism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod handler_register;
mod l1block;

pub use handler_register::{
deduct_caller, end, last_frame_return, load_accounts, optimism_handle_register, output,
reward_beneficiary, validate_env, validate_tx_against_state,
deduct_caller, end, last_frame_return, load_accounts, load_precompiles,
optimism_handle_register, output, reward_beneficiary, validate_env, validate_tx_against_state,
};
pub use l1block::{L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT};
20 changes: 19 additions & 1 deletion crates/revm/src/optimism/handler_register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ use crate::{
db::Database, spec_to_generic, Account, EVMError, Env, ExecutionResult, HaltReason,
HashMap, InvalidTransaction, ResultAndState, Spec, SpecId, SpecId::REGOLITH, U256,
},
Context, FrameResult,
Context, ContextPrecompiles, FrameResult,
};
use core::ops::Mul;
use revm_precompile::{secp256r1, PrecompileSpecId, Precompiles};
use std::string::ToString;
use std::sync::Arc;

Expand All @@ -23,6 +24,8 @@ pub fn optimism_handle_register<DB: Database, EXT>(handler: &mut EvmHandler<'_,
handler.validation.env = Arc::new(validate_env::<SPEC, DB>);
// Validate transaction against state.
handler.validation.tx_against_state = Arc::new(validate_tx_against_state::<SPEC, EXT, DB>);
// Load additional precompiles for the given chain spec.
handler.pre_execution.load_precompiles = Arc::new(load_precompiles::<SPEC, EXT, DB>);
// load l1 data
handler.pre_execution.load_accounts = Arc::new(load_accounts::<SPEC, EXT, DB>);
// An estimated batch cost is charged from the caller and added to L1 Fee Vault.
Expand Down Expand Up @@ -137,6 +140,21 @@ pub fn last_frame_return<SPEC: Spec, EXT, DB: Database>(
Ok(())
}

/// Load precompiles for Optimism chain.
#[inline]
pub fn load_precompiles<SPEC: Spec, EXT, DB: Database>() -> ContextPrecompiles<DB> {
let mut precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(SPEC::SPEC_ID)).clone();

if SPEC::enabled(SpecId::FJORD) {
precompiles.extend([
// EIP-7212: secp256r1 P256verify
secp256r1::P256VERIFY,
])
}

precompiles.into()
}

/// Load account (make them warm) and l1 data from database.
#[inline]
pub fn load_accounts<SPEC: Spec, EXT, DB: Database>(
Expand Down

0 comments on commit 928883c

Please sign in to comment.