diff --git a/Cargo.lock b/Cargo.lock index 9d1315912..1609e6a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.22.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -765,9 +765,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -1072,6 +1072,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "const-hex" version = "1.12.0" @@ -1422,7 +1449,11 @@ dependencies = [ "ark-ec", "ark-ff 0.4.2", "eigen-crypto-bn254", + "eigen-utils", + "ethers", + "eyre", "hex", + "num-bigint", "rand", "thiserror", "tokio", @@ -1440,6 +1471,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "eigen-crypto-keystore" +version = "0.0.1-alpha" +dependencies = [ + "aes", + "ark-bn254", + "ark-ec", + "ark-ff 0.4.2", + "color-eyre", + "ctr", + "eigen-crypto-bls", + "eth-keystore", + "ethers", + "eyre", + "rand", + "scrypt", + "serde", + "serde_json", +] + [[package]] name = "eigen-metrics" version = "0.0.1-alpha" @@ -1881,6 +1932,7 @@ dependencies = [ "const-hex", "enr", "ethers-core", + "futures-channel", "futures-core", "futures-timer", "futures-util", @@ -2256,9 +2308,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -3079,9 +3131,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -3167,6 +3219,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parity-scale-codec" version = "3.6.12" @@ -4116,6 +4174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" dependencies = [ "hmac", + "password-hash", "pbkdf2 0.11.0", "salsa20", "sha2", @@ -4301,6 +4360,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4597,6 +4665,16 @@ dependencies = [ "syn 2.0.67", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -4881,6 +4959,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", ] [[package]] @@ -4893,6 +4982,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 6867c6d32..89b036bc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/chainio/clients/avsregistry/", "crates/crypto/bn254/", "crates/utils/", "crates/crypto/bls/", +"crates/crypto/keystore/", "crates/metrics/collectors/economic/", "crates/metrics/collectors/rpc_calls/", "crates/services/avsregistry/", @@ -69,6 +70,7 @@ eigen-contract-bindings = { path = "crates/contracts/bindings/"} eigen-types = {path = "crates/types/"} eigen-crypto-bls = {path = "crates/crypto/bls/"} eigen-crypto-bn254 = {path = "crates/crypto/bn254/"} +eigen-crypto-keystore = {path = "crates/crypto/keystore/"} eigen-utils = {path = "crates/utils/"} eigen-metrics-collectors-economic = {path = "crates/metrics/collectors/economic"} eigen-metrics-collectors-rpc-calls = {path = "crates/metrics/collectors/rpc_calls"} diff --git a/crates/crypto/bls/Cargo.toml b/crates/crypto/bls/Cargo.toml index 7277d9fb1..d181f8f1c 100644 --- a/crates/crypto/bls/Cargo.toml +++ b/crates/crypto/bls/Cargo.toml @@ -16,6 +16,11 @@ thiserror.workspace = true ark-ec = "0.4.2" alloy-primitives.workspace = true hex = "0.4.3" +eigen-utils.workspace = true +ethers = { version = "2.0", features = ["rustls", "ws"] } +eyre = "0.6.8" +num-bigint = "0.4.4" + [dev-dependencies] rand = "0.8.4" tokio = { workspace = true, features = ["full"] } diff --git a/crates/crypto/bls/src/lib.rs b/crates/crypto/bls/src/lib.rs index 8f058d26c..17172ef0f 100644 --- a/crates/crypto/bls/src/lib.rs +++ b/crates/crypto/bls/src/lib.rs @@ -7,3 +7,174 @@ pub mod attestation; pub mod error; + +use ark_bn254::{Fq, Fr, G1Affine, G2Affine}; +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{ + fields::{Field, PrimeField}, + BigInt, BigInteger, One, +}; +use eigen_utils:: + binding::BLSApkRegistry::{G1Point, G2Point}; +use ethers::{ + core::types::{H256}, + types::Address, + utils::keccak256, +}; +use num_bigint::BigUint; + +pub type PrivateKey = Fr; +pub type PublicKey = G1Affine; +pub type BlsSignature = G1Affine; +pub type OperatorId = H256; + +#[derive(Debug)] +pub struct BlsKeypair { + pub private: PrivateKey, + pub public: PublicKey, +} + +impl From for Option { + fn from(val: BlsKeypair) -> Self { + if let Some((x, y)) = val.public.xy() { + Some(G1Point { + X: alloy_primitives::U256::from_le_slice(&x.into_bigint().to_bytes_le()[..]), + Y: alloy_primitives::U256::from_le_slice(&y.into_bigint().to_bytes_le()[..]), + }) + } else { + None + } + } +} + +impl From for Option { + fn from(val: BlsKeypair) -> Self { + let g2 = val.public_g2(); + if let Some((x, y)) = g2.xy() { + Some(G2Point { + X: [ + alloy_primitives::U256::from_le_slice(&x.c0.into_bigint().to_bytes_le()[..]), + alloy_primitives::U256::from_le_slice(&x.c1.into_bigint().to_bytes_le()[..]), + ], + Y: [ + alloy_primitives::U256::from_le_slice(&y.c0.into_bigint().to_bytes_le()[..]), + alloy_primitives::U256::from_le_slice(&y.c1.into_bigint().to_bytes_le()[..]), + ], + }) + } else { + None + } + } +} + +impl BlsKeypair { + pub fn public_g2(&self) -> G2Affine { + (G2Affine::generator() * self.private).into_affine() + } + + pub fn operator_id(&self) -> OperatorId { + let xy = self.public.xy().expect("should have public"); + keccak256( + [ + xy.0.into_bigint().to_bytes_be(), + xy.1.into_bigint().to_bytes_be(), + ] + .concat() + ).into() + } + + pub fn make_pubkey_registration_data( + &self, + operator_addr: Address, + bls_pubkey_comp_addr: Address, + chain_id: u64, + ) -> eyre::Result { + let bytes = [ + operator_addr.as_bytes(), + bls_pubkey_comp_addr.as_bytes(), + &[0_u8; 24], + &chain_id.to_be_bytes(), + b"EigenLayer_BN254_Pubkey_Registration", + ] + .concat(); + let hash: H256 = keccak256(&bytes).into(); + self.sign(hash.as_bytes()) + } + + pub fn sign(&self, msg: &[u8]) -> eyre::Result { + let h = Self::map_to_curve(msg)?; + let sig = h * self.private; + + Ok(sig.into_affine()) + } + + pub fn sign_hashed(&self, msg: G1Affine) -> eyre::Result { + let sig = msg * self.private; + + Ok(sig.into_affine()) + } + /// implements BN254 map to curve from + /// contracts/lib/eigenlayer-middleware/lib/eigenlayer-contracts/src/contracts/libraries/BN254.sol + /// for a hash, maps to a point on curve + /// y^2 = x^3 + b + fn map_to_curve(hash: &[u8]) -> eyre::Result { + let mut x: Fq = Fq::from_be_bytes_mod_order(hash); + let b = BigInt::<4>::from(3_u32); + + loop { + let beta = x.pow(b) + Fq::from(3_u32); + if let Some(y) = beta.sqrt() { + return Ok(PublicKey::new(x, y)); + } else { + x += Fq::one() + } + } + } +} + + +pub struct EthConvert; +impl EthConvert { + pub fn to_u256(p: &Fq) -> alloy_primitives::U256 { + alloy_primitives::U256::from_le_slice(&p.into_bigint().to_bytes_le()[..]) + } + + pub fn to_g1(xy: G1Affine) -> Option { + xy.xy().map(|(x, y)| G1Point { + X: EthConvert::to_u256(x), + Y: EthConvert::to_u256(y), + }) + } + + pub fn to_g2(xy: G2Affine) -> Option { + xy.xy().map(|(x, y)| G2Point { + X: [EthConvert::to_u256(&x.c1), EthConvert::to_u256(&x.c0)], + Y: [EthConvert::to_u256(&y.c1), EthConvert::to_u256(&y.c0)], + }) + } + + pub fn from_g1(xy: G1Point) -> G1Affine { + let x = BigUint::from_bytes_le(&xy.X.to_le_bytes::<32>()[..]); + let y = BigUint::from_bytes_le(&xy.Y.to_le_bytes::<32>()[..]); + G1Affine::new(x.into(), y.into()) + } +} + + +#[test] +fn test_map_parity() { + use std::str::FromStr; + // taken from golang impl + let x = Fq::from_str( + "21808877952123445795107598745041753552237365029343566086488416315631580963384", + ) + .unwrap(); + let y = Fq::from_str( + "11638128931416599220980524115187668264422283409187640152391635080130668110949", + ) + .unwrap(); + let expected = PublicKey::new(x, y); + let msg = b"07c2ee97b7ae54ffe597b9db97ede3b7"; + let r = BlsKeypair::map_to_curve(msg).unwrap(); + assert_eq!(r, expected); +} \ No newline at end of file diff --git a/crates/crypto/keystore/Cargo.toml b/crates/crypto/keystore/Cargo.toml new file mode 100644 index 000000000..91c939439 --- /dev/null +++ b/crates/crypto/keystore/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "eigen-crypto-keystore" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true + +[dependencies] +aes = "0.8.0" +ark-bn254 = { version = "0.4.0", features = ["std", "curve"] } +ark-ec = "0.4.2" +ark-ff = { version = "0.4.2", features = ["std"] } +color-eyre = "0.6" +ctr = "0.9.0" +eth-keystore = "0.5.0" +ethers = { version = "2.0", features = ["rustls", "ws"] } +eyre = "0.6.8" +rand = "0.8.5" +scrypt = "0.10.0" +serde = { version = "1.0.192", features = ["derive"] } +serde_json = { version = "1.0.85" } +eigen-crypto-bls.workspace = true + diff --git a/crates/crypto/keystore/src/lib.rs b/crates/crypto/keystore/src/lib.rs new file mode 100644 index 000000000..27f1e7068 --- /dev/null +++ b/crates/crypto/keystore/src/lib.rs @@ -0,0 +1,151 @@ +#![doc( + html_logo_url = "https://github.com/supernovahs/eigensdk-rs/assets/91280922/bd13caec-3c00-4afc-839a-b83d2890beb5", + issue_tracker_base_url = "https://github.com/supernovahs/eigen-rs/issues/" +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +use aes::{ + cipher::{InnerIvInit, KeyInit, StreamCipherCore}, + Aes128, +}; +use ark_ec::{AffineRepr, CurveGroup}; +use ark_ff::{fields::PrimeField, Field}; +use eth_keystore::{CryptoJson, KdfparamsType}; +use ethers::{signers::LocalWallet, utils::keccak256, types::H256}; +use eyre::{eyre, Ok, Report}; +use rand::{thread_rng, RngCore}; +use scrypt::{scrypt, Params as ScryptParams}; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, fs::File, io::Read, path::Path}; + +use eigen_crypto_bls::{BlsKeypair, PrivateKey, PublicKey}; + +#[derive(Default)] +pub struct EncodedKeystore { + encrypted_keystore: Option, + password: Option, +} + +impl EncodedKeystore { + pub fn from_path

(path: &P, password: Option) -> eyre::Result + where + P: AsRef, + { + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let keystore: Keystore = serde_json::from_str(&contents)?; + Ok(Self { + encrypted_keystore: Some(keystore), + password, + }) + } + + pub fn from_string(contents: String, password: Option) -> eyre::Result { + let keystore: Keystore = serde_json::from_str(&contents)?; + Ok(Self { + encrypted_keystore: Some(keystore), + password, + }) + } + + pub fn random() -> eyre::Result { + Ok(Self::default()) + } + + pub fn into_bls_keypair(self) -> eyre::Result { + let fr = if let Some(keystore) = self.encrypted_keystore { + let secret = decrypt_key(keystore, self.password.unwrap_or_default())?; + PrivateKey::from_be_bytes_mod_order(&secret) + } else { + let rnd = &mut [0_u8; 32]; + let mut rng = thread_rng(); + loop { + rng.fill_bytes(rnd); + if let Some(key) = PrivateKey::from_random_bytes(rnd) { + break key; + } + } + }; + let p = PublicKey::generator() * fr; + + Ok(BlsKeypair { + private: fr, + public: p.into_affine(), + }) + } + + pub fn into_wallet(self) -> eyre::Result { + if let Some(keystore) = self.encrypted_keystore { + let secret = decrypt_key(keystore, self.password.unwrap_or_default())?; + Ok(LocalWallet::from_bytes(&secret)?) + } else { + Ok(LocalWallet::new(&mut thread_rng())) + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct Keystore { + crypto: CryptoJson, +} + +fn decrypt_key(keystore: Keystore, password: S) -> eyre::Result> +where + S: AsRef<[u8]>, +{ + // Derive the key. + let key = match keystore.crypto.kdfparams { + KdfparamsType::Pbkdf2 { .. } => { + return Err(Report::msg("Pbkdf2 not supported")); + } + KdfparamsType::Scrypt { + dklen, + n, + p, + r, + salt, + } => { + let mut key = vec![0u8; dklen as usize]; + let log_n = (n as f32).log2() as u8; + let scrypt_params = ScryptParams::new(log_n, r, p)?; + scrypt(password.as_ref(), &salt, &scrypt_params, key.as_mut_slice())?; + key + } + }; + + // Derive the MAC from the derived key and ciphertext. + let derived_mac: H256 = keccak256( + [&key[16..32], &keystore.crypto.ciphertext] + .concat() + ).into(); + + if derived_mac.as_bytes() != keystore.crypto.mac.as_slice() { + return Err(eyre!("MacMismatch")); + } + + // Decrypt the private key bytes using AES-128-CTR + let decryptor = + Aes128Ctr::new(&key[..16], &keystore.crypto.cipherparams.iv[..16]).expect("invalid length"); + + let mut pk = keystore.crypto.ciphertext; + decryptor.apply_keystream(&mut pk); + + Ok(pk) +} + +struct Aes128Ctr { + inner: ctr::CtrCore, +} + +impl Aes128Ctr { + fn new(key: &[u8], iv: &[u8]) -> eyre::Result { + let cipher = aes::Aes128::new_from_slice(key).unwrap(); + let inner = ctr::CtrCore::inner_iv_slice_init(cipher, iv)?; + Ok(Self { inner }) + } + + fn apply_keystream(self, buf: &mut [u8]) { + self.inner.apply_keystream_partial(buf.into()); + } +}