staking_cli/
registration.rs

1use alloy::{
2    network::Ethereum,
3    primitives::Address,
4    providers::{PendingTransactionBuilder, Provider},
5};
6use anyhow::Result;
7use hotshot_contract_adapter::{
8    evm::DecodeRevert as _,
9    sol_types::StakeTableV2::{self, StakeTableV2Errors},
10    stake_table::StakeTableContractVersion,
11};
12
13use crate::{
14    metadata::MetadataUri,
15    parse::Commission,
16    signature::{NodeSignatures, NodeSignaturesSol},
17};
18
19pub async fn register_validator(
20    provider: impl Provider,
21    stake_table_addr: Address,
22    commission: Commission,
23    metadata_uri: MetadataUri,
24    payload: NodeSignatures,
25) -> Result<PendingTransactionBuilder<Ethereum>> {
26    tracing::info!(
27        "register validator {} with commission {commission}",
28        payload.address
29    );
30    // NOTE: the StakeTableV2 ABI is a superset of the V1 ABI because the V2 inherits from V1 so we
31    // can always use the V2 bindings for calling functions and decoding events, even if we are
32    // connected to the V1 contract.
33    let stake_table = StakeTableV2::new(stake_table_addr, provider);
34    let sol_payload = NodeSignaturesSol::from(payload);
35
36    let version = stake_table.getVersion().call().await?.try_into()?;
37    // There is a race-condition here if the contract is upgraded while this transactions is waiting
38    // to be mined. We're very unlikely to hit this in practice, and since we only perform the
39    // upgrade on decaf this is acceptable.
40    Ok(match version {
41        StakeTableContractVersion::V1 => stake_table
42            .registerValidator(
43                sol_payload.bls_vk,
44                sol_payload.schnorr_vk,
45                sol_payload.bls_signature.into(),
46                commission.to_evm(),
47            )
48            .send()
49            .await
50            .maybe_decode_revert::<StakeTableV2Errors>()?,
51        StakeTableContractVersion::V2 => stake_table
52            .registerValidatorV2(
53                sol_payload.bls_vk,
54                sol_payload.schnorr_vk,
55                sol_payload.bls_signature.into(),
56                sol_payload.schnorr_signature.into(),
57                commission.to_evm(),
58                metadata_uri.to_string(),
59            )
60            .send()
61            .await
62            .maybe_decode_revert::<StakeTableV2Errors>()?,
63    })
64}
65
66pub async fn update_consensus_keys(
67    provider: impl Provider,
68    stake_table_addr: Address,
69    payload: NodeSignatures,
70) -> Result<PendingTransactionBuilder<Ethereum>> {
71    // NOTE: the StakeTableV2 ABI is a superset of the V1 ABI because the V2 inherits from V1 so we
72    // can always use the V2 bindings for calling functions and decoding events, even if we are
73    // connected to the V1 contract.
74    let stake_table = StakeTableV2::new(stake_table_addr, provider);
75    let sol_payload = NodeSignaturesSol::from(payload);
76
77    // There is a race-condition here if the contract is upgraded while this transactions is waiting
78    // to be mined. We're very unlikely to hit this in practice, and since we only perform the
79    // upgrade on decaf this is acceptable.
80    let version = stake_table.getVersion().call().await?.try_into()?;
81    Ok(match version {
82        StakeTableContractVersion::V1 => stake_table
83            .updateConsensusKeys(
84                sol_payload.bls_vk,
85                sol_payload.schnorr_vk,
86                sol_payload.bls_signature.into(),
87            )
88            .send()
89            .await
90            .maybe_decode_revert::<StakeTableV2Errors>()?,
91        StakeTableContractVersion::V2 => stake_table
92            .updateConsensusKeysV2(
93                sol_payload.bls_vk,
94                sol_payload.schnorr_vk,
95                sol_payload.bls_signature.into(),
96                sol_payload.schnorr_signature.into(),
97            )
98            .send()
99            .await
100            .maybe_decode_revert::<StakeTableV2Errors>()?,
101    })
102}
103
104pub async fn deregister_validator(
105    provider: impl Provider,
106    stake_table_addr: Address,
107) -> Result<PendingTransactionBuilder<Ethereum>> {
108    let stake_table = StakeTableV2::new(stake_table_addr, provider);
109    stake_table
110        .deregisterValidator()
111        .send()
112        .await
113        .maybe_decode_revert::<StakeTableV2Errors>()
114}
115
116pub async fn update_commission(
117    provider: impl Provider,
118    stake_table_addr: Address,
119    new_commission: Commission,
120) -> Result<PendingTransactionBuilder<Ethereum>> {
121    let stake_table = StakeTableV2::new(stake_table_addr, provider);
122    stake_table
123        .updateCommission(new_commission.to_evm())
124        .send()
125        .await
126        .maybe_decode_revert::<StakeTableV2Errors>()
127}
128
129pub async fn update_metadata_uri(
130    provider: impl Provider,
131    stake_table_addr: Address,
132    metadata_uri: MetadataUri,
133) -> Result<PendingTransactionBuilder<Ethereum>> {
134    let stake_table = StakeTableV2::new(stake_table_addr, provider);
135    stake_table
136        .updateMetadataUri(metadata_uri.to_string())
137        .send()
138        .await
139        .maybe_decode_revert::<StakeTableV2Errors>()
140}
141
142pub async fn fetch_commission(
143    provider: impl Provider,
144    stake_table_addr: Address,
145    validator: Address,
146) -> Result<Commission> {
147    let stake_table = StakeTableV2::new(stake_table_addr, provider);
148    let version: StakeTableContractVersion = stake_table.getVersion().call().await?.try_into()?;
149    if matches!(version, StakeTableContractVersion::V1) {
150        anyhow::bail!("fetching commission is not supported with stake table V1");
151    }
152    Ok(stake_table
153        .commissionTracking(validator)
154        .call()
155        .await?
156        .commission
157        .try_into()?)
158}
159
160#[cfg(test)]
161mod test {
162    use alloy::{primitives::U256, providers::WalletProvider as _};
163    use espresso_contract_deployer::build_provider;
164    use espresso_types::{
165        v0_3::{Fetcher, StakeTableEvent},
166        L1Client,
167    };
168    use hotshot_contract_adapter::{
169        sol_types::{EdOnBN254PointSol, G1PointSol, G2PointSol},
170        stake_table::{sign_address_bls, sign_address_schnorr, StateSignatureSol},
171    };
172    use rand::{rngs::StdRng, SeedableRng as _};
173    use rstest::rstest;
174
175    use super::*;
176    use crate::{deploy::TestSystem, receipt::ReceiptExt};
177
178    #[tokio::test]
179    async fn test_register_validator() -> Result<()> {
180        let system = TestSystem::deploy().await?;
181        let validator_address = system.deployer_address;
182        let payload = NodeSignatures::create(
183            validator_address,
184            &system.bls_key_pair,
185            &system.state_key_pair,
186        );
187
188        let metadata_uri = "https://example.com/metadata".parse()?;
189        let receipt = register_validator(
190            &system.provider,
191            system.stake_table,
192            system.commission,
193            metadata_uri,
194            payload,
195        )
196        .await?
197        .assert_success()
198        .await?;
199
200        let event = receipt
201            .decoded_log::<StakeTableV2::ValidatorRegisteredV2>()
202            .unwrap();
203        assert_eq!(event.account, validator_address);
204        assert_eq!(event.commission, system.commission.to_evm());
205        assert_eq!(event.metadataUri, "https://example.com/metadata");
206
207        assert_eq!(event.blsVK, system.bls_key_pair.ver_key().into());
208        assert_eq!(event.schnorrVK, system.state_key_pair.ver_key().into());
209
210        event.data.authenticate()?;
211        Ok(())
212    }
213
214    #[rstest]
215    #[case(StakeTableContractVersion::V1)]
216    #[case(StakeTableContractVersion::V2)]
217    #[tokio::test]
218    async fn test_deregister_validator(#[case] version: StakeTableContractVersion) -> Result<()> {
219        let system = TestSystem::deploy_version(version).await?;
220        system.register_validator().await?;
221
222        let receipt = deregister_validator(&system.provider, system.stake_table)
223            .await?
224            .assert_success()
225            .await?;
226
227        match version {
228            StakeTableContractVersion::V1 => {
229                let event = receipt
230                    .decoded_log::<StakeTableV2::ValidatorExit>()
231                    .unwrap();
232                assert_eq!(event.validator, system.deployer_address);
233            },
234            StakeTableContractVersion::V2 => {
235                let event = receipt
236                    .decoded_log::<StakeTableV2::ValidatorExitV2>()
237                    .unwrap();
238                assert_eq!(event.validator, system.deployer_address);
239                let block = system
240                    .provider
241                    .get_block_by_number(receipt.block_number.unwrap().into())
242                    .await?
243                    .unwrap();
244                let expected_unlock = block.header.timestamp + system.exit_escrow_period.as_secs();
245                assert_eq!(event.unlocksAt, U256::from(expected_unlock));
246            },
247        }
248
249        Ok(())
250    }
251
252    #[tokio::test]
253    async fn test_update_consensus_keys() -> Result<()> {
254        let system = TestSystem::deploy().await?;
255        system.register_validator().await?;
256        let validator_address = system.deployer_address;
257        let mut rng = StdRng::from_seed([43u8; 32]);
258        let (_, new_bls, new_schnorr) = TestSystem::gen_keys(&mut rng);
259        let payload = NodeSignatures::create(validator_address, &new_bls, &new_schnorr);
260
261        let receipt = update_consensus_keys(&system.provider, system.stake_table, payload)
262            .await?
263            .assert_success()
264            .await?;
265
266        let event = receipt
267            .decoded_log::<StakeTableV2::ConsensusKeysUpdatedV2>()
268            .unwrap();
269        assert_eq!(event.account, system.deployer_address);
270
271        assert_eq!(event.blsVK, new_bls.ver_key().into());
272        assert_eq!(event.schnorrVK, new_schnorr.ver_key().into());
273
274        event.data.authenticate()?;
275
276        Ok(())
277    }
278
279    #[tokio::test]
280    async fn test_update_commission() -> Result<()> {
281        let system = TestSystem::deploy().await?;
282
283        // Set commission update interval to 1 second for testing
284        let stake_table = StakeTableV2::new(system.stake_table, &system.provider);
285        stake_table
286            .setMinCommissionUpdateInterval(U256::from(1)) // 1 second
287            .send()
288            .await?
289            .assert_success()
290            .await?;
291
292        system.register_validator().await?;
293        let validator_address = system.deployer_address;
294        let new_commission = Commission::try_from("10.50")?;
295
296        // Wait 2 seconds to ensure we're past the interval
297        system.anvil_increase_time(U256::from(2)).await?;
298
299        let receipt = update_commission(&system.provider, system.stake_table, new_commission)
300            .await?
301            .assert_success()
302            .await?;
303
304        let event = receipt
305            .decoded_log::<StakeTableV2::CommissionUpdated>()
306            .unwrap();
307        assert_eq!(event.validator, validator_address);
308        assert_eq!(event.newCommission, new_commission.to_evm());
309
310        let fetched_commission =
311            fetch_commission(&system.provider, system.stake_table, validator_address).await?;
312        assert_eq!(fetched_commission, new_commission);
313
314        Ok(())
315    }
316
317    /// The GCL must remove stake table events with incorrect signatures. This test verifies that a
318    /// validator registered event with incorrect schnorr signature is removed before the stake
319    /// table is computed.
320    #[tokio::test]
321    async fn test_integration_unauthenticated_validator_registered_events_removed() -> Result<()> {
322        let system = TestSystem::deploy().await?;
323
324        // register a validator with correct signature
325        system.register_validator().await?;
326
327        // NOTE: we can't register a validator with a bad BLS signature because the contract will revert
328
329        let provider = build_provider(
330            "test test test test test test test test test test test junk",
331            1,
332            system.rpc_url.clone(),
333            /* polling_interval */ None,
334        );
335        let validator_address = provider.default_signer_address();
336        let (_, bls_key_pair, schnorr_key_pair) =
337            TestSystem::gen_keys(&mut StdRng::from_seed([1u8; 32]));
338        let (_, _, other_schnorr_key_pair) =
339            TestSystem::gen_keys(&mut StdRng::from_seed([2u8; 32]));
340
341        let bls_vk = G2PointSol::from(bls_key_pair.ver_key());
342        let bls_sig = G1PointSol::from(sign_address_bls(&bls_key_pair, validator_address));
343        let schnorr_vk = EdOnBN254PointSol::from(schnorr_key_pair.ver_key());
344
345        // create a valid schnorr signature with the *wrong* key
346        let schnorr_sig_other_key = StateSignatureSol::from(sign_address_schnorr(
347            &other_schnorr_key_pair,
348            validator_address,
349        ));
350
351        let stake_table = StakeTableV2::new(system.stake_table, provider);
352
353        // register a validator with the schnorr sig from another key
354        let receipt = stake_table
355            .registerValidatorV2(
356                bls_vk,
357                schnorr_vk,
358                bls_sig.into(),
359                schnorr_sig_other_key.into(),
360                Commission::try_from("12.34")?.to_evm(),
361                "https://example.com/metadata".to_string(),
362            )
363            .send()
364            .await
365            .maybe_decode_revert::<StakeTableV2Errors>()?
366            .assert_success()
367            .await?;
368
369        let l1 = L1Client::new(vec![system.rpc_url])?;
370        let events = Fetcher::fetch_events_from_contract(
371            l1,
372            system.stake_table,
373            Some(0),
374            receipt.block_number.unwrap(),
375        )
376        .await?;
377
378        // verify that we only have the first RegisterV2 event
379        assert_eq!(events.len(), 1);
380        match events[0].1.clone() {
381            StakeTableEvent::RegisterV2(event) => {
382                assert_eq!(event.account, system.deployer_address);
383            },
384            _ => panic!("expected RegisterV2 event"),
385        }
386        Ok(())
387    }
388
389    /// The GCL must remove stake table events with incorrect signatures. This test verifies that a
390    /// consensus keys update event with incorrect schnorr signature is removed before the stake
391    /// table is computed.
392    #[tokio::test]
393    async fn test_integration_unauthenticated_update_consensus_keys_events_removed() -> Result<()> {
394        let system = TestSystem::deploy().await?;
395
396        // register a validator with correct signature
397        system.register_validator().await?;
398        let validator_address = system.deployer_address;
399
400        // NOTE: we can't register a validator with a bad BLS signature because the contract will revert
401
402        let (_, new_bls_key_pair, new_schnorr_key_pair) =
403            TestSystem::gen_keys(&mut StdRng::from_seed([1u8; 32]));
404        let (_, _, other_schnorr_key_pair) =
405            TestSystem::gen_keys(&mut StdRng::from_seed([2u8; 32]));
406
407        let bls_vk = G2PointSol::from(new_bls_key_pair.ver_key());
408        let bls_sig = G1PointSol::from(sign_address_bls(&new_bls_key_pair, validator_address));
409        let schnorr_vk = EdOnBN254PointSol::from(new_schnorr_key_pair.ver_key());
410
411        // create a valid schnorr signature with the *wrong* key
412        let schnorr_sig_other_key = StateSignatureSol::from(sign_address_schnorr(
413            &other_schnorr_key_pair,
414            validator_address,
415        ))
416        .into();
417
418        let stake_table = StakeTableV2::new(system.stake_table, system.provider);
419
420        // update consensus keys with the schnorr sig from another key
421        let receipt = stake_table
422            .updateConsensusKeysV2(bls_vk, schnorr_vk, bls_sig.into(), schnorr_sig_other_key)
423            .send()
424            .await
425            .maybe_decode_revert::<StakeTableV2Errors>()?
426            .assert_success()
427            .await?;
428
429        let l1 = L1Client::new(vec![system.rpc_url])?;
430        let events = Fetcher::fetch_events_from_contract(
431            l1,
432            system.stake_table,
433            Some(0),
434            receipt.block_number.unwrap(),
435        )
436        .await?;
437
438        // verify that we only have the RegisterV2 event
439        assert_eq!(events.len(), 1);
440        match events[0].1.clone() {
441            StakeTableEvent::RegisterV2(event) => {
442                assert_eq!(event.account, system.deployer_address);
443            },
444            _ => panic!("expected RegisterV2 event"),
445        }
446
447        println!("Events: {events:?}");
448
449        Ok(())
450    }
451
452    #[tokio::test]
453    async fn test_update_metadata_uri() -> Result<()> {
454        let system = TestSystem::deploy().await?;
455        system.register_validator().await?;
456
457        let new_uri: MetadataUri = "https://example.com/updated".parse()?;
458        let receipt = update_metadata_uri(&system.provider, system.stake_table, new_uri.clone())
459            .await?
460            .assert_success()
461            .await?;
462
463        let event = receipt
464            .decoded_log::<StakeTableV2::MetadataUriUpdated>()
465            .unwrap();
466        assert_eq!(event.validator, system.deployer_address);
467        assert_eq!(event.metadataUri, new_uri.to_string());
468
469        Ok(())
470    }
471
472    #[tokio::test]
473    async fn test_register_validator_with_empty_metadata_uri() -> Result<()> {
474        let system = TestSystem::deploy().await?;
475        let validator_address = system.deployer_address;
476        let payload = NodeSignatures::create(
477            validator_address,
478            &system.bls_key_pair,
479            &system.state_key_pair,
480        );
481
482        let metadata_uri = MetadataUri::empty();
483        let receipt = register_validator(
484            &system.provider,
485            system.stake_table,
486            system.commission,
487            metadata_uri,
488            payload,
489        )
490        .await?
491        .assert_success()
492        .await?;
493
494        let event = receipt
495            .decoded_log::<StakeTableV2::ValidatorRegisteredV2>()
496            .unwrap();
497        assert_eq!(event.account, validator_address);
498        assert_eq!(event.commission, system.commission.to_evm());
499        assert_eq!(event.metadataUri, "");
500
501        Ok(())
502    }
503
504    #[tokio::test]
505    async fn test_update_metadata_uri_to_empty() -> Result<()> {
506        let system = TestSystem::deploy().await?;
507        system.register_validator().await?;
508
509        let metadata_uri = MetadataUri::empty();
510        let receipt = update_metadata_uri(&system.provider, system.stake_table, metadata_uri)
511            .await?
512            .assert_success()
513            .await?;
514
515        let event = receipt
516            .decoded_log::<StakeTableV2::MetadataUriUpdated>()
517            .unwrap();
518        assert_eq!(event.validator, system.deployer_address);
519        assert_eq!(event.metadataUri, "");
520
521        Ok(())
522    }
523}