From e9c5e9dcf344803fff5f0d6bdda45a42ba076f60 Mon Sep 17 00:00:00 2001 From: Damian Ramirez Date: Thu, 13 Feb 2025 17:38:18 -0300 Subject: [PATCH] feat: new method `register_operator_with_churn` (#354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What Changed? Add new method `register_operator_with_churn` to `avsregistry/writer`. Close #304 ### Reviewer Checklist - [ ] New features are tested and documented - [ ] PR updates the changelog with a description of changes - [ ] PR has one of the `changelog-X` labels (if applies) - [ ] Code deprecates any old functionality before removing it --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com> --- CHANGELOG.md | 28 ++ .../chainio/clients/avsregistry/src/writer.rs | 257 +++++++++++++++++- testing/testing-utils/src/anvil_constants.rs | 11 + 3 files changed, 291 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91aaf4a3..70a7de82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,34 @@ Those changes in added, changed or breaking changes, should include usage exampl ### Added 🎉 +* Added new method `register_operator_with_churn` in `avsregistry/writer` in [#354](https://github.com/Layr-Labs/eigensdk-rs/pull/354). + + ```rust + let bls_key_pair = BlsKeyPair::new(BLS_KEY).unwrap(); + let operator_sig_salt = FixedBytes::from([0x02; 32]); + let operator_sig_expiry = U256::MAX; + let quorum_nums = Bytes::from([0]); + let socket = "socket".to_string(); + let churn_sig_salt = FixedBytes::from([0x05; 32]); + let churn_sig_expiry = U256::MAX; + + + let tx_hash = avs_writer_2 + .register_operator_with_churn( + bls_key_pair, // Operator's BLS key pair + operator_sig_salt, // Operator signature salt + operator_sig_expiry, // Operator signature expiry + quorum_nums, // Quorum numbers for registration + socket, // Socket address + vec![REGISTERED_OPERATOR], // Operators to kick if quorum is full + CHURN_PRIVATE_KEY, // Churn approver's private key + churn_sig_salt, // Churn signature salt + churn_sig_expiry, // Churn signature expiry + ) + .await + .unwrap(); + ``` + * Added new method `set_churn_approver` in `avsregistry/writer` in [#333](https://github.com/Layr-Labs/eigensdk-rs/pull/333). ```rust diff --git a/crates/chainio/clients/avsregistry/src/writer.rs b/crates/chainio/clients/avsregistry/src/writer.rs index ad6d848e..60497d08 100644 --- a/crates/chainio/clients/avsregistry/src/writer.rs +++ b/crates/chainio/clients/avsregistry/src/writer.rs @@ -10,12 +10,14 @@ use eigen_crypto_bls::{ alloy_g1_point_to_g1_affine, convert_to_g1_point, convert_to_g2_point, BlsKeyPair, }; use eigen_logging::logger::SharedLogger; +use eigen_types::operator::operator_id_from_g1_pub_key; use eigen_types::operator::QuorumNum; use eigen_utils::convert_stake_registry_strategy_params_to_registry_coordinator_strategy_params; +use eigen_utils::slashing::middleware::registrycoordinator::IBLSApkRegistryTypes::PubkeyRegistrationParams; +use eigen_utils::slashing::middleware::registrycoordinator::ISlashingRegistryCoordinatorTypes::OperatorKickParam; use eigen_utils::slashing::middleware::registrycoordinator::ISlashingRegistryCoordinatorTypes::OperatorSetParam; use eigen_utils::slashing::middleware::registrycoordinator::{ - IBLSApkRegistryTypes::PubkeyRegistrationParams, ISignatureUtils::SignatureWithSaltAndExpiry, - RegistryCoordinator, + ISignatureUtils::SignatureWithSaltAndExpiry, RegistryCoordinator, }; use eigen_utils::slashing::middleware::stakeregistry::IStakeRegistryTypes::StrategyParams; use eigen_utils::slashing::middleware::{ @@ -196,6 +198,154 @@ impl AvsRegistryChainWriter { Ok(*tx.tx_hash()) } + /// Registers an operator while replacing existing operators in full quorums. If any quorum reaches + /// its maximum operator capacity, `operatorKickParams` is used to replace an old operator with the new one. + /// + /// # Arguments + /// + /// * `bls_key_pair` - bls key pair of the operator + /// * `operator_to_avs_registration_sig_salt` - operator signature salt + /// * `operator_to_avs_registration_sig_expiry` - operator signature expiry + /// * `quorum_numbers` - quorum numbers to register the new operator + /// * `socket` - socket used for calling the contract with `registerOperator` function + /// * `operators_to_kick` - operators to kick if quorum is full + /// * `churn_signer_private_key` - private key of the churn signer + /// * `churn_sig_salt` - churn signature salt + /// * `churn_sig_expiry` - churn signature expiry + /// + /// # Returns + /// + /// * `TxHash` - transaction hash of the register operator with churn transaction + #[allow(clippy::too_many_arguments)] + pub async fn register_operator_with_churn( + &self, + bls_key_pair: BlsKeyPair, + operator_to_avs_registration_sig_salt: FixedBytes<32>, + operator_to_avs_registration_sig_expiry: U256, + quorum_numbers: Bytes, + socket: String, + operators_to_kick: Vec
, + churn_signer_private_key: String, + churn_sig_salt: FixedBytes<32>, + churn_sig_expiry: U256, + ) -> Result { + let provider = get_signer(&self.signer.clone(), &self.provider); + let operator_wallet = PrivateKeySigner::from_str(&self.signer) + .map_err(|_| AvsRegistryError::InvalidPrivateKey)?; + let operator_address = operator_wallet.address(); + + info!( + avs_service_manager = %self.service_manager_addr, + operator = %operator_address, + quorum_numbers = ?quorum_numbers, + "registering operator with churn the AVS's registry coordinator" + ); + + let contract_registry_coordinator = + RegistryCoordinator::new(self.registry_coordinator_addr, &provider); + + let g1_hashed_msg_to_sign = contract_registry_coordinator + .pubkeyRegistrationMessageHash(operator_address) + .call() + .await + .map_err(|_| AvsRegistryError::PubKeyRegistrationMessageHash)? + ._0; + + let sig = bls_key_pair + .sign_hashed_to_curve_message(alloy_g1_point_to_g1_affine(g1_hashed_msg_to_sign)) + .g1_point(); + let alloy_g1_point_signed_msg = convert_to_g1_point(sig.g1())?; + let g1_pub_key_bn254 = convert_to_g1_point(bls_key_pair.public_key().g1())?; + let g2_pub_key_bn254 = convert_to_g2_point(bls_key_pair.public_key_g2().g2())?; + + let pub_key_reg_params = PubkeyRegistrationParams { + pubkeyRegistrationSignature: alloy_g1_point_signed_msg.clone(), + pubkeyG1: g1_pub_key_bn254.clone(), + pubkeyG2: g2_pub_key_bn254.clone(), + }; + + let msg_to_sign = self + .el_reader + .calculate_operator_avs_registration_digest_hash( + operator_address, + self.service_manager_addr, + operator_to_avs_registration_sig_salt, + operator_to_avs_registration_sig_expiry, + ) + .await?; + + let operator_signature = operator_wallet + .sign_hash(&msg_to_sign) + .await + .map_err(|_| AvsRegistryError::InvalidSignature)?; + + let operator_signature_with_salt_and_expiry = SignatureWithSaltAndExpiry { + signature: operator_signature.as_bytes().into(), + salt: operator_to_avs_registration_sig_salt, + expiry: operator_to_avs_registration_sig_expiry, + }; + + let operators_to_kick_params: Vec = operators_to_kick + .iter() + .zip(quorum_numbers.iter()) + .map(|(address, quorum_number)| OperatorKickParam { + operator: *address, + quorumNumber: *quorum_number, + }) + .collect(); + + let operator_id = FixedBytes::from( + operator_id_from_g1_pub_key(bls_key_pair.public_key()) + .map_err(|_| AvsRegistryError::GetOperatorId)?, + ); + + let churn_wallet = PrivateKeySigner::from_str(&churn_signer_private_key) + .map_err(|_| AvsRegistryError::InvalidPrivateKey)?; + + let churn_digest_hash = contract_registry_coordinator + .calculateOperatorChurnApprovalDigestHash( + operator_address, + operator_id, + operators_to_kick_params.clone(), + churn_sig_salt, + churn_sig_expiry, + ) + .call() + .await? + ._0; + + let churn_signature = churn_wallet + .sign_hash(&churn_digest_hash) + .await + .map_err(|_| AvsRegistryError::InvalidSignature)?; + + let churn_signature_with_salt_and_expiry = SignatureWithSaltAndExpiry { + signature: churn_signature.as_bytes().into(), + salt: churn_sig_salt, + expiry: churn_sig_expiry, + }; + + let contract_call = contract_registry_coordinator.registerOperatorWithChurn( + quorum_numbers, + socket, + pub_key_reg_params, + operators_to_kick_params, + churn_signature_with_salt_and_expiry, + operator_signature_with_salt_and_expiry, + ); + + let tx = contract_call + .send() + .await + .map_err(AvsRegistryError::AlloyContractError)?; + + info!( + tx_hash = ?tx.tx_hash(), + "Sent transaction to register operator with churn in the AVS's registry coordinator" + ); + Ok(*tx.tx_hash()) + } + /// Updates the stake of their entire operator set /// /// Is used by avs teams running https://github.com/Layr-Labs/avs-sync to updates @@ -811,14 +961,19 @@ mod tests { test_deregister_operator, test_register_operator, }; use alloy::primitives::aliases::U96; - use alloy::primitives::U256; - use alloy::primitives::{address, Address, Bytes}; + use alloy::primitives::{address, Address, Bytes, FixedBytes, U256}; use alloy::sol_types::SolCall; use eigen_common::{get_provider, get_signer}; + use eigen_crypto_bls::BlsKeyPair; use eigen_testing_utils::anvil::{start_anvil_container, start_m2_anvil_container}; - use eigen_testing_utils::anvil_constants::get_allocation_manager_address; use eigen_testing_utils::anvil_constants::get_erc20_mock_strategy; use eigen_testing_utils::anvil_constants::get_service_manager_address; + use eigen_testing_utils::anvil_constants::SECOND_PRIVATE_KEY; + use eigen_testing_utils::anvil_constants::THIRD_ADDRESS; + use eigen_testing_utils::anvil_constants::THIRD_PRIVATE_KEY; + use eigen_testing_utils::anvil_constants::{ + get_allocation_manager_address, OPERATOR_BLS_KEY_2, + }; use eigen_testing_utils::anvil_constants::{ FIFTH_ADDRESS, FIFTH_PRIVATE_KEY, FIRST_ADDRESS, FIRST_PRIVATE_KEY, OPERATOR_BLS_KEY, SECOND_ADDRESS, @@ -1117,6 +1272,98 @@ mod tests { ); } + #[tokio::test] + async fn test_register_operator_with_churn() { + let (_container, http_endpoint, _ws_endpoint) = start_m2_anvil_container().await; + let bls_key = OPERATOR_BLS_KEY.to_string(); + let private_key = FIRST_PRIVATE_KEY.to_string(); + let quorum_nums = Bytes::from([0]); + let avs_writer = + build_avs_registry_chain_writer(http_endpoint.clone(), private_key.clone()).await; + let avs_reader = build_avs_registry_chain_reader(http_endpoint.clone()).await; + + test_register_operator( + &avs_writer, + bls_key.clone(), + quorum_nums.clone(), + http_endpoint.clone(), + ) + .await; + + let is_registered = avs_reader + .is_operator_registered(FIRST_ADDRESS) + .await + .unwrap(); + assert!(is_registered); + + let operator_set_params = OperatorSetParam { + maxOperatorCount: 1, + kickBIPsOfOperatorStake: 10, + kickBIPsOfTotalStake: 10000, + }; + let tx_hash = avs_writer + .set_operator_set_param(0, operator_set_params.clone()) + .await + .unwrap(); + let tx_status = wait_transaction(&http_endpoint, tx_hash) + .await + .unwrap() + .status(); + assert!(tx_status); + + let tx_hash = avs_writer.set_churn_approver(THIRD_ADDRESS).await.unwrap(); + let tx_status = wait_transaction(&http_endpoint, tx_hash) + .await + .unwrap() + .status(); + assert!(tx_status); + + let avs_writer_2 = + build_avs_registry_chain_writer(http_endpoint.clone(), SECOND_PRIVATE_KEY.to_string()) + .await; + let bls_key_2 = OPERATOR_BLS_KEY_2.to_string(); + + let operator_sig_salt = FixedBytes::from([0x02; 32]); + let churn_sig_salt = FixedBytes::from([0x05; 32]); + let sig_expiry = U256::MAX; + let churn_private_key = THIRD_PRIVATE_KEY.to_string(); + + let tx_hash = avs_writer_2 + .register_operator_with_churn( + BlsKeyPair::new(bls_key_2).unwrap(), + operator_sig_salt, + sig_expiry, + quorum_nums.clone(), + "socket".to_string(), + vec![FIRST_ADDRESS], + churn_private_key, + churn_sig_salt, + sig_expiry, + ) + .await + .unwrap(); + + let tx_status = wait_transaction(&http_endpoint, tx_hash) + .await + .unwrap() + .status(); + assert!(tx_status); + + let avs_reader = build_avs_registry_chain_reader(http_endpoint.clone()).await; + let is_registered = avs_reader + .is_operator_registered(FIRST_ADDRESS) + .await + .unwrap(); + assert!(!is_registered); + + let avs_reader = build_avs_registry_chain_reader(http_endpoint.clone()).await; + let is_registered = avs_reader + .is_operator_registered(SECOND_ADDRESS) + .await + .unwrap(); + assert!(is_registered); + } + #[tokio::test] async fn test_set_minimum_stake_for_quorum() { let (_container, http_endpoint, _ws_endpoint) = start_m2_anvil_container().await; diff --git a/testing/testing-utils/src/anvil_constants.rs b/testing/testing-utils/src/anvil_constants.rs index 9a9f33d5..0af93bb6 100644 --- a/testing/testing-utils/src/anvil_constants.rs +++ b/testing/testing-utils/src/anvil_constants.rs @@ -17,6 +17,13 @@ pub const SECOND_ADDRESS: Address = address!("70997970C51812dc3A010C7d01b50e0d17 pub const SECOND_PRIVATE_KEY: &str = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; +/// Address of the third default account generated by anvil +pub const THIRD_ADDRESS: Address = address!("3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); + +/// Private key of the third default account generated by anvil +pub const THIRD_PRIVATE_KEY: &str = + "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"; + /// Address of the fifth default account generated by anvil pub const FIFTH_ADDRESS: Address = address!("9965507D1a55bcC2695C58ba16FB37d819B0A4dc"); /// Private key of the fifth default account generated by anvil @@ -30,6 +37,10 @@ pub const CONTRACTS_REGISTRY: Address = address!("5FbDB2315678afecb367f032d93F64 pub const OPERATOR_BLS_KEY: &str = "1371012690269088913462269866874713266643928125698382731338806296762673180359922"; +/// Bls private key 2 +pub const OPERATOR_BLS_KEY_2: &str = + "15610126902690889134622698668747132666439281256983827313388062967626731803501"; + /// Local anvil rpc http url pub const ANVIL_HTTP_URL: &str = "http://localhost:8545";