Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a51c0f4

Browse files
committedOct 22, 2024·
Apply consensus committee changes at epoch boundaries
The most important changes in this commit are in `deposit.sol`. Everything else is pretty much just supporting the new interfaces in the deposit contract. The key feature is that we now keep track of the current 'epoch' and only apply changes to the consensus committee two epochs after they are made. There are a few other changes to the deposit contract in this commit too: * The initial stakers are now passed to the contract constructor, rather than relying on the `setStake` hack. * `leader()` was removed since it isn't used anywhere and it doesn't make sense to have both `leader` and `leaderAtView`. * Delayed withdrawals are now supported - We keep a circular buffer of pending withdrawals for each validator. 2 weeks after unstaking, the validator can make another call to withdraw the funds from the deposit contract.
1 parent a75f40c commit a51c0f4

File tree

14 files changed

+83290
-39068
lines changed

14 files changed

+83290
-39068
lines changed
 

‎z2/src/converter.rs

-41
Original file line numberDiff line numberDiff line change
@@ -319,47 +319,6 @@ pub async fn convert_persistence(
319319
}
320320
}
321321

322-
// Add stake for this validator. For now, we just assume they've always had 64 ZIL staked.
323-
// This assumptions will need to change for the actual testnet and mainnet launches, where we cannot invent ZIL
324-
// out of thin air (like we do below).
325-
let data = contracts::deposit::SET_STAKE.encode_input(&[
326-
Token::Bytes(secret_key.node_public_key().as_bytes()),
327-
Token::Bytes(
328-
secret_key
329-
.to_libp2p_keypair()
330-
.public()
331-
.to_peer_id()
332-
.to_bytes(),
333-
),
334-
Token::Address(ethabi::Address::from_low_u64_be(1)),
335-
Token::Uint((64 * 10u128.pow(18)).into()),
336-
])?;
337-
let (
338-
ResultAndState {
339-
result,
340-
state: result_state,
341-
},
342-
..,
343-
) = state.apply_transaction_evm(
344-
Address::ZERO,
345-
Some(contract_addr::DEPOSIT),
346-
0,
347-
node_config.consensus.eth_block_gas_limit,
348-
0,
349-
data,
350-
None,
351-
BlockHeader::default(),
352-
inspector::noop(),
353-
BaseFeeCheck::Ignore,
354-
)?;
355-
if !result.is_success() {
356-
return Err(anyhow!("setting stake failed: {result:?}"));
357-
}
358-
state.apply_delta_evm(&result_state)?;
359-
360-
// Flush any pending changes to db
361-
let _ = state.root_hash()?;
362-
363322
if !convert_blocks {
364323
println!("Accounts converted. Skipping blocks.");
365324
return Ok(());

‎z2/src/setup.rs

+9-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use alloy::{
99
};
1010
use anyhow::{anyhow, Context, Result};
1111
use k256::ecdsa::SigningKey;
12-
use libp2p::PeerId;
1312
use serde::{Deserialize, Serialize};
1413
use serde_yaml;
1514
use tera::Tera;
@@ -26,9 +25,8 @@ use zilliqa::{
2625
local_address_default, max_blocks_in_flight_default,
2726
minimum_time_left_for_empty_block_default, scilla_address_default,
2827
scilla_ext_libs_path_default, scilla_stdlib_dir_default, state_rpc_limit_default,
29-
total_native_token_supply_default, Amount, ConsensusConfig,
28+
total_native_token_supply_default, Amount, ConsensusConfig, GenesisDeposit,
3029
},
31-
crypto::NodePublicKey,
3230
transaction::EvmGas,
3331
};
3432

@@ -281,7 +279,7 @@ impl Setup {
281279

282280
pub async fn generate_config(&self) -> Result<()> {
283281
// The genesis deposits.
284-
let mut genesis_deposits: Vec<(NodePublicKey, PeerId, Amount, Address)> = Vec::new();
282+
let mut genesis_deposits: Vec<GenesisDeposit> = Vec::new();
285283
for (node, desc) in self.config.shape.nodes.iter() {
286284
if desc.is_validator {
287285
let data = self
@@ -291,12 +289,13 @@ impl Setup {
291289
.ok_or(anyhow!("no node data for {node}"))?;
292290
// Better have a genesis deposit.
293291
let secret_key = SecretKey::from_hex(&data.secret_key)?;
294-
genesis_deposits.push((
295-
secret_key.node_public_key(),
296-
secret_key.to_libp2p_keypair().public().to_peer_id(),
297-
GENESIS_DEPOSIT.into(),
298-
data.address,
299-
))
292+
genesis_deposits.push(GenesisDeposit {
293+
public_key: secret_key.node_public_key(),
294+
peer_id: secret_key.to_libp2p_keypair().public().to_peer_id(),
295+
stake: GENESIS_DEPOSIT.into(),
296+
reward_address: data.address,
297+
control_address: data.address,
298+
});
300299
}
301300
}
302301

‎zilliqa-macros/src/test.rs

+37-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use syn::{parse::Parser, ItemFn};
77
pub(crate) fn test_macro(args: TokenStream, item: TokenStream) -> TokenStream {
88
let mut restrict_concurrency = false;
99
let mut do_checkpoints = false;
10+
let mut blocks_per_epoch = 10;
1011

1112
let parsed_args =
1213
match syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
@@ -35,6 +36,41 @@ pub(crate) fn test_macro(args: TokenStream, item: TokenStream) -> TokenStream {
3536
}
3637
}
3738
}
39+
syn::Meta::NameValue(a) => {
40+
let Some(name) = a.path.get_ident() else {
41+
return token_stream_with_error(
42+
args,
43+
syn::Error::new_spanned(a.path, "Attribute parameter must be ident"),
44+
);
45+
};
46+
match name.to_string().as_str() {
47+
"blocks_per_epoch" => {
48+
let syn::Expr::Lit(syn::ExprLit {
49+
lit: syn::Lit::Int(val),
50+
..
51+
}) = a.value
52+
else {
53+
return token_stream_with_error(
54+
args,
55+
syn::Error::new_spanned(
56+
a.value,
57+
"Attribute parameter value must be an int",
58+
),
59+
);
60+
};
61+
match val.base10_parse::<u64>() {
62+
Ok(val) => blocks_per_epoch = val,
63+
Err(e) => return token_stream_with_error(args, e),
64+
};
65+
}
66+
_ => {
67+
return token_stream_with_error(
68+
args,
69+
syn::Error::new_spanned(a, "Unknown attribute"),
70+
)
71+
}
72+
}
73+
}
3874
// Can match syn::Meta::Namevalue(a) here for args with params, e.g. to set node_count
3975
// in the future
4076
other => {
@@ -199,7 +235,7 @@ pub(crate) fn test_macro(args: TokenStream, item: TokenStream) -> TokenStream {
199235
async move {
200236
let mut rng = <rand_chacha::ChaCha8Rng as rand_core::SeedableRng>::seed_from_u64(seed);
201237
let network = crate::Network::new(std::sync::Arc::new(std::sync::Mutex::new(rng)), 4, seed, format!("http://{addr}"),
202-
scilla_stdlib_dir.to_string(), #do_checkpoints);
238+
scilla_stdlib_dir.to_string(), #do_checkpoints, #blocks_per_epoch);
203239

204240
// Call the original test function, wrapped in `catch_unwind` so we can detect the panic.
205241
let result = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(

‎zilliqa/src/cfg.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ pub struct ConsensusConfig {
233233
/// The initially staked deposits in the deposit contract at genesis, composed of
234234
/// (public key, peerId, amount, reward address) tuples.
235235
#[serde(default)]
236-
pub genesis_deposits: Vec<(NodePublicKey, PeerId, Amount, Address)>,
236+
pub genesis_deposits: Vec<GenesisDeposit>,
237237
/// Accounts that will be pre-funded at genesis.
238238
#[serde(default)]
239239
pub genesis_accounts: Vec<(Address, Amount)>,
@@ -281,6 +281,16 @@ pub struct ConsensusConfig {
281281
pub total_native_token_supply: Amount,
282282
}
283283

284+
#[derive(Debug, Clone, Serialize, Deserialize)]
285+
#[serde(deny_unknown_fields)]
286+
pub struct GenesisDeposit {
287+
pub public_key: NodePublicKey,
288+
pub peer_id: PeerId,
289+
pub stake: Amount,
290+
pub reward_address: Address,
291+
pub control_address: Address,
292+
}
293+
284294
pub fn consensus_timeout_default() -> Duration {
285295
Duration::from_secs(5)
286296
}

‎zilliqa/src/consensus.rs

+39-36
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,7 @@ impl Consensus {
950950
);
951951
return Ok(None);
952952
}
953+
953954
let committee = self.state.get_stakers_at_block(&block)?;
954955

955956
// verify the sender's signature on block_hash
@@ -2007,42 +2008,40 @@ impl Consensus {
20072008
}
20082009
}
20092010

2010-
if self.block_is_first_in_epoch(block.number()) && !block.is_genesis() {
2011-
// TODO: handle epochs (#1140)
2012-
2013-
if self.config.do_checkpoints
2014-
&& self.epoch_is_checkpoint(self.epoch_number(block.number()))
2015-
{
2016-
if let Some(checkpoint_path) = self.db.get_checkpoint_dir()? {
2017-
let parent = self
2018-
.db
2019-
.get_block_by_hash(block.parent_hash())?
2020-
.ok_or(anyhow!(
2021-
"Trying to checkpoint block, but we don't have its parent"
2011+
if self.block_is_first_in_epoch(block.number())
2012+
&& !block.is_genesis()
2013+
&& self.config.do_checkpoints
2014+
&& self.epoch_is_checkpoint(self.epoch_number(block.number()))
2015+
{
2016+
if let Some(checkpoint_path) = self.db.get_checkpoint_dir()? {
2017+
let parent = self
2018+
.db
2019+
.get_block_by_hash(block.parent_hash())?
2020+
.ok_or(anyhow!(
2021+
"Trying to checkpoint block, but we don't have its parent"
2022+
))?;
2023+
let transactions: Vec<SignedTransaction> = block
2024+
.transactions
2025+
.iter()
2026+
.map(|txn_hash| {
2027+
let tx = self.db.get_transaction(txn_hash)?.ok_or(anyhow!(
2028+
"failed to fetch transaction {} for checkpoint parent {}",
2029+
txn_hash,
2030+
parent.hash()
20222031
))?;
2023-
let transactions: Vec<SignedTransaction> = block
2024-
.transactions
2025-
.iter()
2026-
.map(|txn_hash| {
2027-
let tx = self.db.get_transaction(txn_hash)?.ok_or(anyhow!(
2028-
"failed to fetch transaction {} for checkpoint parent {}",
2029-
txn_hash,
2030-
parent.hash()
2031-
))?;
2032-
Ok::<_, anyhow::Error>(tx)
2033-
})
2034-
.collect::<Result<Vec<SignedTransaction>>>()?;
2035-
2036-
self.message_sender.send_message_to_coordinator(
2037-
InternalMessage::ExportBlockCheckpoint(
2038-
Box::new(block),
2039-
transactions,
2040-
Box::new(parent),
2041-
self.db.state_trie()?.clone(),
2042-
checkpoint_path,
2043-
),
2044-
)?;
2045-
}
2032+
Ok::<_, anyhow::Error>(tx)
2033+
})
2034+
.collect::<Result<Vec<SignedTransaction>>>()?;
2035+
2036+
self.message_sender.send_message_to_coordinator(
2037+
InternalMessage::ExportBlockCheckpoint(
2038+
Box::new(block),
2039+
transactions,
2040+
Box::new(parent),
2041+
self.db.state_trie()?.clone(),
2042+
checkpoint_path,
2043+
),
2044+
)?;
20462045
}
20472046
}
20482047

@@ -2463,7 +2462,11 @@ impl Consensus {
24632462
return None;
24642463
};
24652464

2466-
let public_key = state_at.leader(view).unwrap();
2465+
let executed_block = BlockHeader {
2466+
number: block.header.number + 1,
2467+
..Default::default()
2468+
};
2469+
let public_key = state_at.leader(view, executed_block).unwrap();
24672470
let peer_id = state_at.get_peer_id(public_key).unwrap().unwrap();
24682471

24692472
Some(Validator {

‎zilliqa/src/contracts/compiled.json

+82,452-38,707
Large diffs are not rendered by default.

‎zilliqa/src/contracts/deposit.sol

+422-144
Large diffs are not rendered by default.

‎zilliqa/src/contracts/mod.rs

+17-5
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,36 @@ pub mod deposit {
1414
pub static BYTECODE: Lazy<Vec<u8>> = Lazy::new(|| CONTRACT.bytecode.clone());
1515
pub static LEADER_AT_VIEW: Lazy<Function> =
1616
Lazy::new(|| CONTRACT.abi.function("leaderAtView").unwrap().clone());
17-
pub static TEMP_REMOVE_STAKER: Lazy<Function> =
18-
Lazy::new(|| CONTRACT.abi.function("tempRemoveStaker").unwrap().clone());
1917
pub static DEPOSIT: Lazy<Function> =
2018
Lazy::new(|| CONTRACT.abi.function("deposit").unwrap().clone());
21-
pub static SET_STAKE: Lazy<Function> =
22-
Lazy::new(|| CONTRACT.abi.function("setStake").unwrap().clone());
19+
pub static DEPOSIT_TOPUP: Lazy<Function> =
20+
Lazy::new(|| CONTRACT.abi.function("depositTopup").unwrap().clone());
21+
pub static UNSTAKE: Lazy<Function> =
22+
Lazy::new(|| CONTRACT.abi.function("unstake").unwrap().clone());
23+
pub static WITHDRAW_FUNDS: Lazy<Function> =
24+
Lazy::new(|| CONTRACT.abi.function("withdrawFunds").unwrap().clone());
25+
pub static EJECT: Lazy<Function> =
26+
Lazy::new(|| CONTRACT.abi.function("eject").unwrap().clone());
27+
pub static CURRENT_EPOCH: Lazy<Function> =
28+
Lazy::new(|| CONTRACT.abi.function("currentEpoch").unwrap().clone());
2329
pub static GET_STAKE: Lazy<Function> =
2430
Lazy::new(|| CONTRACT.abi.function("getStake").unwrap().clone());
2531
pub static GET_REWARD_ADDRESS: Lazy<Function> =
2632
Lazy::new(|| CONTRACT.abi.function("getRewardAddress").unwrap().clone());
2733
pub static GET_PEER_ID: Lazy<Function> =
2834
Lazy::new(|| CONTRACT.abi.function("getPeerId").unwrap().clone());
35+
pub static GET_STAKERS_MAP: Lazy<Function> =
36+
Lazy::new(|| CONTRACT.abi.function("getStakersMap").unwrap().clone());
2937
pub static GET_STAKERS: Lazy<Function> =
3038
Lazy::new(|| CONTRACT.abi.function("getStakers").unwrap().clone());
3139
pub static TOTAL_STAKE: Lazy<Function> =
3240
Lazy::new(|| CONTRACT.abi.function("totalStake").unwrap().clone());
3341
pub static MIN_DEPOSIT: Lazy<Function> =
34-
Lazy::new(|| CONTRACT.abi.function("_minimumStake").unwrap().clone());
42+
Lazy::new(|| CONTRACT.abi.function("minimumStake").unwrap().clone());
43+
pub static COMMITTEE: Lazy<Function> =
44+
Lazy::new(|| CONTRACT.abi.function("committee").unwrap().clone());
45+
pub static INTERNAL_COMMITTEE: Lazy<Function> =
46+
Lazy::new(|| CONTRACT.abi.function("internalCommittee").unwrap().clone());
3547
}
3648

3749
pub mod shard {

‎zilliqa/src/exec.rs

+22-8
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,7 @@ impl State {
422422
self.apply_delta_evm(&state)?;
423423
Ok(addr)
424424
}
425-
ExecutionResult::Success { .. } => {
426-
Err(anyhow!("deployment did not create a transaction"))
427-
}
425+
ExecutionResult::Success { .. } => Err(anyhow!("deployment did not create a contract")),
428426
ExecutionResult::Revert { .. } => Err(anyhow!("deployment reverted")),
429427
ExecutionResult::Halt { reason, .. } => Err(anyhow!("deployment halted: {reason:?}")),
430428
}
@@ -715,15 +713,15 @@ impl State {
715713
Ok(())
716714
}
717715

718-
pub fn leader(&self, view: u64) -> Result<NodePublicKey> {
716+
pub fn leader(&self, view: u64, current_block: BlockHeader) -> Result<NodePublicKey> {
719717
let data = contracts::deposit::LEADER_AT_VIEW.encode_input(&[Token::Uint(view.into())])?;
720718

721719
let leader = self.call_contract(
722720
Address::ZERO,
723721
Some(contract_addr::DEPOSIT),
724722
data,
725723
0,
726-
BlockHeader::default(),
724+
current_block,
727725
)?;
728726

729727
NodePublicKey::from_bytes(
@@ -754,7 +752,7 @@ impl State {
754752
0,
755753
// The current block is not accessed when the native balance is read, so we just pass in some
756754
// dummy values.
757-
BlockHeader::default(),
755+
BlockHeader::default(), // TODO
758756
)?;
759757

760758
let stakers = contracts::deposit::GET_STAKERS
@@ -770,6 +768,22 @@ impl State {
770768
.collect()
771769
}
772770

771+
pub fn committee(&self) -> Result<()> {
772+
let data = contracts::deposit::COMMITTEE.encode_input(&[])?;
773+
774+
let committee = self.call_contract(
775+
Address::ZERO,
776+
Some(contract_addr::DEPOSIT),
777+
data,
778+
0,
779+
BlockHeader::default(),
780+
)?;
781+
let committee = contracts::deposit::COMMITTEE.decode_output(&committee)?;
782+
info!("committee: {committee:?}");
783+
784+
Ok(())
785+
}
786+
773787
pub fn get_stake(&self, public_key: NodePublicKey) -> Result<Option<NonZeroU128>> {
774788
let data =
775789
contracts::deposit::GET_STAKE.encode_input(&[Token::Bytes(public_key.as_bytes())])?;
@@ -781,7 +795,7 @@ impl State {
781795
0,
782796
// The current block is not accessed when the native balance is read, so we just pass in some
783797
// dummy values.
784-
BlockHeader::default(),
798+
BlockHeader::default(), // TODO
785799
)?;
786800

787801
let stake = NonZeroU128::new(U256::from_be_slice(&stake).to());
@@ -844,7 +858,7 @@ impl State {
844858
0,
845859
// The current block is not accessed when the native balance is read, so we just pass in some
846860
// dummy values.
847-
BlockHeader::default(),
861+
BlockHeader::default(), // TODO
848862
)?;
849863

850864
let amount = contracts::deposit::TOTAL_STAKE.decode_output(&return_value)?[0]

‎zilliqa/src/state.rs

+62-41
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use alloy::{
1111
use anyhow::{anyhow, Result};
1212
use eth_trie::{EthTrie as PatriciaTrie, Trie};
1313
use ethabi::Token;
14-
use revm::primitives::ResultAndState;
1514
use serde::{Deserialize, Serialize};
1615
use sha3::{Digest, Keccak256};
1716

@@ -20,9 +19,7 @@ use crate::{
2019
cfg::{Amount, NodeConfig, ScillaExtLibsPath},
2120
contracts, crypto,
2221
db::TrieStorage,
23-
exec::BaseFeeCheck,
24-
inspector,
25-
message::{BlockHeader, MAX_COMMITTEE_SIZE},
22+
message::MAX_COMMITTEE_SIZE,
2623
node::ChainId,
2724
scilla::{ParamValue, Scilla, Transition},
2825
serde_util::vec_param_value,
@@ -130,12 +127,13 @@ impl State {
130127
.fold(0, |acc, item: &(Address, Amount)| acc + item.1 .0),
131128
)
132129
.expect("Genesis accounts sum to more than total native token supply")
133-
.checked_sub(config.consensus.genesis_deposits.iter().fold(
134-
0,
135-
|acc, item: &(crypto::NodePublicKey, libp2p::PeerId, Amount, Address)| {
136-
acc + item.2 .0
137-
},
138-
))
130+
.checked_sub(
131+
config
132+
.consensus
133+
.genesis_deposits
134+
.iter()
135+
.fold(0, |acc, item| acc + item.stake.0),
136+
)
139137
.expect(
140138
"Genesis accounts + genesis deposits sum to more than total native token supply",
141139
);
@@ -151,46 +149,69 @@ impl State {
151149
})?;
152150
}
153151

152+
let initial_stakers: Vec<_> = config
153+
.consensus
154+
.genesis_deposits
155+
.into_iter()
156+
.map(|deposit| {
157+
Token::Tuple(vec![
158+
Token::Bytes(deposit.public_key.as_bytes()),
159+
Token::Bytes(deposit.peer_id.to_bytes()),
160+
Token::Address(ethabi::Address::from(deposit.reward_address.into_array())),
161+
Token::Address(ethabi::Address::from(deposit.control_address.into_array())),
162+
Token::Uint((*deposit.stake).into()),
163+
])
164+
})
165+
.collect();
154166
let deposit_data = contracts::deposit::CONSTRUCTOR.encode_input(
155167
contracts::deposit::BYTECODE.to_vec(),
156168
&[
157169
Token::Uint((*config.consensus.minimum_stake).into()),
158170
Token::Uint(MAX_COMMITTEE_SIZE.into()),
171+
Token::Uint(config.consensus.blocks_per_epoch.into()),
172+
Token::Array(initial_stakers),
159173
],
160174
)?;
161-
162175
state.force_deploy_contract_evm(deposit_data, Some(contract_addr::DEPOSIT))?;
163176

164-
for (pub_key, peer_id, stake, reward_address) in config.consensus.genesis_deposits {
165-
let data = contracts::deposit::SET_STAKE.encode_input(&[
166-
Token::Bytes(pub_key.as_bytes()),
167-
Token::Bytes(peer_id.to_bytes()),
168-
Token::Address(ethabi::Address::from(reward_address.into_array())),
169-
Token::Uint((*stake).into()),
170-
])?;
171-
let (
172-
ResultAndState {
173-
result,
174-
state: result_state,
175-
},
176-
..,
177-
) = state.apply_transaction_evm(
178-
Address::ZERO,
179-
Some(contract_addr::DEPOSIT),
180-
0,
181-
config.consensus.eth_block_gas_limit,
182-
0,
183-
data,
184-
None,
185-
BlockHeader::default(),
186-
inspector::noop(),
187-
BaseFeeCheck::Ignore,
188-
)?;
189-
if !result.is_success() {
190-
return Err(anyhow!("setting stake failed: {result:?}"));
191-
}
192-
state.apply_delta_evm(&result_state)?;
193-
}
177+
//for GenesisDeposit {
178+
// public_key,
179+
// peer_id,
180+
// stake,
181+
// reward_address,
182+
// control_address,
183+
//} in config.consensus.genesis_deposits
184+
//{
185+
// let data = contracts::deposit::SET_STAKE.encode_input(&[
186+
// Token::Bytes(public_key.as_bytes()),
187+
// Token::Bytes(peer_id.to_bytes()),
188+
// Token::Address(ethabi::Address::from(reward_address.into_array())),
189+
// Token::Address(ethabi::Address::from(control_address.into_array())),
190+
// Token::Uint((*stake).into()),
191+
// ])?;
192+
// let (
193+
// ResultAndState {
194+
// result,
195+
// state: result_state,
196+
// },
197+
// ..,
198+
// ) = state.apply_transaction_evm(
199+
// Address::ZERO,
200+
// Some(contract_addr::DEPOSIT),
201+
// 0,
202+
// config.consensus.eth_block_gas_limit,
203+
// 0,
204+
// data,
205+
// None,
206+
// BlockHeader::default(),
207+
// inspector::noop(),
208+
// BaseFeeCheck::Ignore,
209+
// )?;
210+
// if !result.is_success() {
211+
// return Err(anyhow!("setting stake failed: {result:?}"));
212+
// }
213+
// state.apply_delta_evm(&result_state)?;
214+
//}
194215

195216
Ok(state)
196217
}

‎zilliqa/tests/it/consensus.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ async fn create_shard(
152152
network.scilla_address.clone(),
153153
network.scilla_stdlib_dir.clone(),
154154
false,
155+
2,
155156
);
156157
let shard_wallet = shard_network.genesis_wallet().await;
157158

@@ -658,7 +659,7 @@ async fn zero_account_per_block_balance_updates(mut network: Network) {
658659
.consensus
659660
.genesis_deposits
660661
.clone();
661-
let total_staked: u128 = genesis_deposits[0].2 .0 * 4;
662+
let total_staked: u128 = genesis_deposits[0].stake.0 * 4;
662663

663664
// Zero account balance plus genesis account plus initial stakes should equal total_native_token_supply
664665
let zero_account_balance: u128 = wallet

‎zilliqa/tests/it/main.rs

+28-20
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ use zilliqa::{
6969
max_blocks_in_flight_default, minimum_time_left_for_empty_block_default,
7070
scilla_address_default, scilla_ext_libs_path_default, scilla_stdlib_dir_default,
7171
state_rpc_limit_default, total_native_token_supply_default, Amount, Checkpoint,
72-
ConsensusConfig, NodeConfig,
72+
ConsensusConfig, GenesisDeposit, NodeConfig,
7373
},
74-
crypto::{NodePublicKey, SecretKey, TransactionPublicKey},
74+
crypto::{SecretKey, TransactionPublicKey},
7575
db,
7676
message::{ExternalMessage, InternalMessage},
7777
node::{Node, RequestId},
@@ -209,7 +209,7 @@ struct TestNode {
209209
}
210210

211211
struct Network {
212-
pub genesis_deposits: Vec<(NodePublicKey, PeerId, Amount, Address)>,
212+
pub genesis_deposits: Vec<GenesisDeposit>,
213213
/// Child shards.
214214
pub children: HashMap<u64, Network>,
215215
pub shard_id: u64,
@@ -238,9 +238,12 @@ struct Network {
238238
scilla_address: String,
239239
scilla_stdlib_dir: String,
240240
do_checkpoints: bool,
241+
blocks_per_epoch: u64,
241242
}
242243

243244
impl Network {
245+
// This is only used in the zilliqa_macros::test macro. Consider refactoring this to a builder
246+
// or removing entirely (and calling new_shard there)?
244247
/// Create a main shard network with reasonable defaults.
245248
pub fn new(
246249
rng: Arc<Mutex<ChaCha8Rng>>,
@@ -249,6 +252,7 @@ impl Network {
249252
scilla_address: String,
250253
scilla_stdlib_dir: String,
251254
do_checkpoints: bool,
255+
blocks_per_epoch: u64,
252256
) -> Network {
253257
Self::new_shard(
254258
rng,
@@ -260,6 +264,7 @@ impl Network {
260264
scilla_address,
261265
scilla_stdlib_dir,
262266
do_checkpoints,
267+
blocks_per_epoch,
263268
)
264269
}
265270

@@ -274,6 +279,7 @@ impl Network {
274279
scilla_address: String,
275280
scilla_stdlib_dir: String,
276281
do_checkpoints: bool,
282+
blocks_per_epoch: u64,
277283
) -> Network {
278284
let mut signing_keys = keys.unwrap_or_else(|| {
279285
(0..nodes)
@@ -296,13 +302,13 @@ impl Network {
296302
let stake = 32_000_000_000_000_000_000u128;
297303
let genesis_deposits: Vec<_> = keys
298304
.iter()
299-
.map(|k| {
300-
(
301-
k.0.node_public_key(),
302-
k.0.to_libp2p_keypair().public().to_peer_id(),
303-
stake.into(),
304-
TransactionPublicKey::Ecdsa(*k.1.verifying_key(), true).into_addr(),
305-
)
305+
.map(|k| GenesisDeposit {
306+
public_key: k.0.node_public_key(),
307+
peer_id: k.0.to_libp2p_keypair().public().to_peer_id(),
308+
stake: stake.into(),
309+
reward_address: TransactionPublicKey::Ecdsa(*k.1.verifying_key(), true).into_addr(),
310+
control_address: TransactionPublicKey::Ecdsa(*k.1.verifying_key(), true)
311+
.into_addr(),
306312
})
307313
.collect();
308314

@@ -326,7 +332,7 @@ impl Network {
326332
eth_block_gas_limit: EvmGas(84000000),
327333
gas_price: 4_761_904_800_000u128.into(),
328334
main_shard_id: None,
329-
blocks_per_epoch: 10,
335+
blocks_per_epoch,
330336
epochs_per_checkpoint: 1,
331337
total_native_token_supply: total_native_token_supply_default(),
332338
},
@@ -398,6 +404,7 @@ impl Network {
398404
genesis_key,
399405
scilla_address,
400406
do_checkpoints,
407+
blocks_per_epoch,
401408
scilla_stdlib_dir,
402409
}
403410
}
@@ -444,7 +451,7 @@ impl Network {
444451
main_shard_id: None,
445452
minimum_time_left_for_empty_block: minimum_time_left_for_empty_block_default(),
446453
scilla_address: scilla_address_default(),
447-
blocks_per_epoch: 10,
454+
blocks_per_epoch: self.blocks_per_epoch,
448455
epochs_per_checkpoint: 1,
449456
scilla_stdlib_dir: scilla_stdlib_dir_default(),
450457
scilla_ext_libs_path: scilla_ext_libs_path_default(),
@@ -493,13 +500,13 @@ impl Network {
493500
let stake = 32_000_000_000_000_000_000u128;
494501
let genesis_deposits: Vec<_> = keys
495502
.iter()
496-
.map(|k| {
497-
(
498-
k.0.node_public_key(),
499-
k.0.to_libp2p_keypair().public().to_peer_id(),
500-
stake.into(),
501-
TransactionPublicKey::Ecdsa(*k.1.verifying_key(), true).into_addr(),
502-
)
503+
.map(|k| GenesisDeposit {
504+
public_key: k.0.node_public_key(),
505+
peer_id: k.0.to_libp2p_keypair().public().to_peer_id(),
506+
stake: stake.into(),
507+
reward_address: TransactionPublicKey::Ecdsa(*k.1.verifying_key(), true).into_addr(),
508+
control_address: TransactionPublicKey::Ecdsa(*k.1.verifying_key(), true)
509+
.into_addr(),
503510
})
504511
.collect();
505512

@@ -551,7 +558,7 @@ impl Network {
551558
minimum_time_left_for_empty_block:
552559
minimum_time_left_for_empty_block_default(),
553560
scilla_address: scilla_address_default(),
554-
blocks_per_epoch: 10,
561+
blocks_per_epoch: self.blocks_per_epoch,
555562
epochs_per_checkpoint: 1,
556563
scilla_stdlib_dir: scilla_stdlib_dir_default(),
557564
scilla_ext_libs_path: scilla_ext_libs_path_default(),
@@ -867,6 +874,7 @@ impl Network {
867874
self.scilla_address.clone(),
868875
self.scilla_stdlib_dir.clone(),
869876
self.do_checkpoints,
877+
self.blocks_per_epoch,
870878
),
871879
);
872880
}

‎zilliqa/tests/it/persistence.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ use zilliqa::{
1414
failed_request_sleep_duration_default, json_rpc_port_default, max_blocks_in_flight_default,
1515
minimum_time_left_for_empty_block_default, scilla_address_default,
1616
scilla_ext_libs_path_default, scilla_stdlib_dir_default, state_rpc_limit_default,
17-
total_native_token_supply_default, Checkpoint,
17+
total_native_token_supply_default, Checkpoint, ConsensusConfig, NodeConfig,
1818
},
1919
crypto::{Hash, SecretKey},
2020
transaction::EvmGas,
2121
};
2222

23-
use crate::{deploy_contract, ConsensusConfig, Network, NewNodeOptions, NodeConfig, TestNode};
23+
use crate::{deploy_contract, Network, NewNodeOptions, TestNode};
2424

2525
#[zilliqa_macros::test]
2626
async fn block_and_tx_data_persistence(mut network: Network) {
@@ -75,6 +75,7 @@ async fn block_and_tx_data_persistence(mut network: Network) {
7575
let block_with_tx = inner.get_block(receipt.block_hash).unwrap().unwrap();
7676
let last_block = inner.get_block(last_number).unwrap().unwrap();
7777
let tx = inner.get_transaction_by_hash(hash).unwrap().unwrap();
78+
7879
// sanity check
7980
assert_eq!(tx.hash, hash);
8081
assert_eq!(block_with_tx.transactions.len(), 1);

‎zilliqa/tests/it/staking.rs

+186-51
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ethers::{
99
types::{BlockId, BlockNumber, TransactionRequest},
1010
};
1111
use libp2p::PeerId;
12-
use primitive_types::H160;
12+
use primitive_types::{H160, H256};
1313
use rand::Rng;
1414
use tracing::{info, trace};
1515
use zilliqa::{contracts, crypto::NodePublicKey, state::contract_addr};
@@ -36,17 +36,21 @@ async fn check_miner_got_reward(
3636

3737
async fn deposit_stake(
3838
network: &mut Network,
39-
wallet: &SignerMiddleware<Provider<LocalRpcClient>, LocalWallet>,
39+
control_wallet: &SignerMiddleware<Provider<LocalRpcClient>, LocalWallet>,
4040
key: NodePublicKey,
4141
peer_id: PeerId,
4242
stake: u128,
4343
reward_address: H160,
4444
pop: blsful::ProofOfPossession<Bls12381G2Impl>,
45-
) {
45+
) -> H256 {
4646
// Transfer the new validator enough ZIL to stake.
4747
let tx = TransactionRequest::pay(reward_address, stake);
48-
let hash = wallet.send_transaction(tx, None).await.unwrap().tx_hash();
49-
network.run_until_receipt(wallet, hash, 80).await;
48+
let hash = control_wallet
49+
.send_transaction(tx, None)
50+
.await
51+
.unwrap()
52+
.tx_hash();
53+
network.run_until_receipt(control_wallet, hash, 80).await;
5054

5155
// Stake the new validator's funds.
5256
let tx = TransactionRequest::new()
@@ -62,20 +66,77 @@ async fn deposit_stake(
6266
])
6367
.unwrap(),
6468
);
65-
let hash = wallet.send_transaction(tx, None).await.unwrap().tx_hash();
66-
network.run_until_receipt(wallet, hash, 80).await;
69+
let hash = control_wallet
70+
.send_transaction(tx, None)
71+
.await
72+
.unwrap()
73+
.tx_hash();
74+
network.run_until_receipt(control_wallet, hash, 80).await;
75+
hash
76+
}
77+
78+
async fn current_epoch(
79+
wallet: &SignerMiddleware<Provider<LocalRpcClient>, LocalWallet>,
80+
block: Option<u64>,
81+
) -> u64 {
82+
let tx = TransactionRequest::new()
83+
.to(H160(contract_addr::DEPOSIT.into_array()))
84+
.data(contracts::deposit::CURRENT_EPOCH.encode_input(&[]).unwrap());
85+
let response = wallet
86+
.call(&tx.into(), block.map(|b| b.into()))
87+
.await
88+
.unwrap();
89+
let epoch = contracts::deposit::CURRENT_EPOCH
90+
.decode_output(&response)
91+
.unwrap()
92+
.remove(0)
93+
.into_uint()
94+
.unwrap()
95+
.as_u64();
96+
let current_block = block.unwrap_or(wallet.get_block_number().await.unwrap().as_u64());
97+
98+
// Sanity check that epochs are calculated correctly (assuming `blocks_per_epoch = 2`).
99+
assert_eq!(epoch, current_block / 2);
100+
101+
epoch
67102
}
68103

69-
async fn remove_staker(network: &mut Network, wallet: &Wallet, key: NodePublicKey) {
104+
async fn unstake_amount(network: &mut Network, control_wallet: &Wallet, amount: u128) -> H256 {
70105
let tx = TransactionRequest::new()
71106
.to(H160(contract_addr::DEPOSIT.into_array()))
72107
.data(
73-
contracts::deposit::TEMP_REMOVE_STAKER
74-
.encode_input(&[Token::Bytes(key.as_bytes())])
108+
contracts::deposit::UNSTAKE
109+
.encode_input(&[Token::Uint(amount.into())])
110+
.unwrap(),
111+
)
112+
.gas(10000000); // TODO: Why needed?
113+
let hash = control_wallet
114+
.send_transaction(tx, None)
115+
.await
116+
.unwrap()
117+
.tx_hash();
118+
let receipt = network.run_until_receipt(control_wallet, hash, 100).await;
119+
assert_eq!(receipt.status.unwrap().as_u64(), 1);
120+
hash
121+
}
122+
123+
async fn get_stake(wallet: &Wallet, staker: &NodePublicKey) -> u128 {
124+
let tx = TransactionRequest::new()
125+
.to(H160(contract_addr::DEPOSIT.into_array()))
126+
.data(
127+
contracts::deposit::GET_STAKE
128+
.encode_input(&[Token::Bytes(staker.as_bytes())])
75129
.unwrap(),
76130
);
77-
let hash = wallet.send_transaction(tx, None).await.unwrap().tx_hash();
78-
network.run_until_receipt(wallet, hash, 50).await;
131+
let output = wallet.call(&tx.into(), None).await.unwrap();
132+
133+
contracts::deposit::GET_STAKE
134+
.decode_output(&output)
135+
.unwrap()[0]
136+
.clone()
137+
.into_uint()
138+
.unwrap()
139+
.as_u128()
79140
}
80141

81142
async fn get_stakers(
@@ -145,10 +206,16 @@ async fn rewards_are_sent_to_reward_address_of_proposer(mut network: Network) {
145206
check_miner_got_reward(&wallet, 1).await;
146207
}
147208

148-
#[zilliqa_macros::test]
209+
#[zilliqa_macros::test(blocks_per_epoch = 2)]
149210
async fn validators_can_join_and_become_proposer(mut network: Network) {
150211
let wallet = network.genesis_wallet().await;
151212

213+
// randomise the current epoch state and current leader
214+
let blocks_to_prerun = network.rng.lock().unwrap().gen_range(0..8);
215+
network
216+
.run_until_block(&wallet, blocks_to_prerun.into(), 100)
217+
.await;
218+
152219
let index = network.add_node();
153220
let new_validator_key = network.get_node_raw(index).secret_key;
154221
let reward_address = H160::random_using(&mut network.rng.lock().unwrap().deref_mut());
@@ -157,22 +224,50 @@ async fn validators_can_join_and_become_proposer(mut network: Network) {
157224
assert_eq!(stakers.len(), 4);
158225
assert!(!stakers.contains(&new_validator_key.node_public_key()));
159226

160-
let pop = new_validator_key.pop_prove();
161-
162-
deposit_stake(
227+
let deposit_hash = deposit_stake(
163228
&mut network,
164229
&wallet,
165230
new_validator_key.node_public_key(),
166231
new_validator_key.to_libp2p_keypair().public().to_peer_id(),
167232
32 * 10u128.pow(18),
168233
reward_address,
169-
pop,
234+
new_validator_key.pop_prove(),
170235
)
171236
.await;
172237

173-
let stakers = get_stakers(&wallet).await;
174-
assert_eq!(stakers.len(), 5);
175-
assert!(stakers.contains(&new_validator_key.node_public_key()));
238+
let deposit_block = wallet
239+
.get_transaction_receipt(deposit_hash)
240+
.await
241+
.unwrap()
242+
.unwrap()
243+
.block_number
244+
.unwrap()
245+
.as_u64();
246+
247+
// The new validator should become part of the committee exactly two epochs after the one in which the deposit was
248+
// made.
249+
let deposit_epoch = current_epoch(&wallet, Some(deposit_block)).await;
250+
network
251+
.run_until_async(
252+
|| async {
253+
let should_be_in_committee =
254+
current_epoch(&wallet, None).await == deposit_epoch + 2;
255+
256+
let stakers = get_stakers(&wallet).await;
257+
if !should_be_in_committee {
258+
assert_eq!(stakers.len(), 4);
259+
assert!(!stakers.contains(&new_validator_key.node_public_key()));
260+
false // Keep running
261+
} else {
262+
assert_eq!(stakers.len(), 5);
263+
assert!(stakers.contains(&new_validator_key.node_public_key()));
264+
true
265+
}
266+
},
267+
200,
268+
)
269+
.await
270+
.unwrap();
176271

177272
// Check the new validator eventually gets to be a block proposer.
178273
network
@@ -268,45 +363,85 @@ async fn block_proposers_are_selected_proportionally_to_their_stake(mut network:
268363
);
269364
}
270365

271-
#[zilliqa_macros::test]
272-
async fn validators_can_leave(mut network: Network) {
273-
let genesis_wallet = network.genesis_wallet().await;
366+
#[zilliqa_macros::test(blocks_per_epoch = 2)]
367+
async fn validators_can_unstake(mut network: Network) {
368+
let wallet = network.genesis_wallet().await;
274369

275-
let blocks_to_prerun = network.rng.lock().unwrap().gen_range(0..5);
370+
// randomise the current epoch state and current leader
371+
let blocks_to_prerun = network.rng.lock().unwrap().gen_range(0..8);
276372
network
277-
.run_until_block(&genesis_wallet, blocks_to_prerun.into(), 100)
373+
.run_until_block(&wallet, blocks_to_prerun.into(), 100)
278374
.await;
279375

280-
let validator_to_remove = network.random_index();
281-
let validator_sending_removal = network.random_index();
282-
283-
let key_to_remove = network.get_node_raw(validator_to_remove).secret_key;
284-
let wallet_sending_removal = network
285-
.wallet_from_key(
286-
network
287-
.get_node_raw(validator_sending_removal)
288-
.onchain_key
289-
.clone(),
290-
)
376+
let validator_idx = network.random_index();
377+
let validator_blskey = network
378+
.get_node_raw(validator_idx)
379+
.secret_key
380+
.node_public_key();
381+
let validator_control_wallet = network
382+
.wallet_from_key(network.get_node_raw(validator_idx).onchain_key.clone())
291383
.await;
292-
fund_wallet(&mut network, &genesis_wallet, &wallet_sending_removal).await;
384+
fund_wallet(&mut network, &wallet, &validator_control_wallet).await;
293385

294-
let stakers = get_stakers(&genesis_wallet).await;
386+
let stakers = get_stakers(&wallet).await;
295387
assert_eq!(stakers.len(), 4);
296-
assert!(stakers.contains(&key_to_remove.node_public_key()));
297-
298-
remove_staker(
299-
&mut network,
300-
&wallet_sending_removal,
301-
key_to_remove.node_public_key(),
302-
)
303-
.await;
388+
assert!(stakers.contains(&validator_blskey));
304389

305-
let stakers = get_stakers(&genesis_wallet).await;
306-
assert_eq!(stakers.len(), 3);
307-
assert!(!stakers.contains(&key_to_remove.node_public_key()));
390+
// unstake validator's entire stake
391+
let stake = get_stake(&wallet, &validator_blskey).await;
392+
let unstake_hash = unstake_amount(&mut network, &validator_control_wallet, stake).await;
393+
let unstake_block = wallet
394+
.get_transaction_receipt(unstake_hash)
395+
.await
396+
.unwrap()
397+
.unwrap()
398+
.block_number
399+
.unwrap()
400+
.as_u64();
401+
402+
// The validator should leave the committee exactly two epochs after the one in which the deposit was made.
403+
let unstake_epoch = current_epoch(&wallet, Some(unstake_block)).await;
404+
network
405+
.run_until_async(
406+
|| async {
407+
let should_be_in_committee =
408+
current_epoch(&wallet, None).await != unstake_epoch + 2;
409+
410+
let stakers = get_stakers(&wallet).await;
411+
if should_be_in_committee {
412+
assert_eq!(stakers.len(), 4);
413+
assert!(stakers.contains(&validator_blskey));
414+
false // Keep running
415+
} else {
416+
assert_eq!(stakers.len(), 3);
417+
assert!(!stakers.contains(&validator_blskey));
418+
true
419+
}
420+
},
421+
200,
422+
)
423+
.await
424+
.unwrap();
308425

426+
// ensure network still runs well
309427
network
310-
.run_until_block(&genesis_wallet, (blocks_to_prerun + 10).into(), 500)
311-
.await;
428+
.run_until_async(
429+
|| async {
430+
let stakers = get_stakers(&wallet).await;
431+
assert_eq!(stakers.len(), 3);
432+
assert!(!stakers.contains(&validator_blskey));
433+
wallet.get_block_number().await.unwrap().as_u64() >= unstake_block + 15
434+
},
435+
1000,
436+
)
437+
.await
438+
.unwrap();
312439
}
440+
441+
// TODO: Tests for:
442+
// * partial unstaking staying above the minimum
443+
// * partial unstaking under the minimum (should fail)
444+
// * increase stake
445+
// * updating staker details (reward address)
446+
// * disallow access to callers other than the controlAddress
447+
// * withdraw stake after 2 weeks (exercise the circular buffer logic or test it separately if difficult to do here)

0 commit comments

Comments
 (0)
Please sign in to comment.