Skip to content

Commit

Permalink
feat: new method register_operator_with_churn (#354)
Browse files Browse the repository at this point in the history
### 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>
  • Loading branch information
damiramirez and MegaRedHand authored Feb 13, 2025
1 parent 2bf52be commit e9c5e9d
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 5 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
257 changes: 252 additions & 5 deletions crates/chainio/clients/avsregistry/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Address>,
churn_signer_private_key: String,
churn_sig_salt: FixedBytes<32>,
churn_sig_expiry: U256,
) -> Result<TxHash, AvsRegistryError> {
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<OperatorKickParam> = 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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions testing/testing-utils/src/anvil_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";

Expand Down

0 comments on commit e9c5e9d

Please # to comment.