diff --git a/consensus/src/user/provisioners.rs b/consensus/src/user/provisioners.rs index 3cdad75ec6..1c1ff18fcd 100644 --- a/consensus/src/user/provisioners.rs +++ b/consensus/src/user/provisioners.rs @@ -24,6 +24,12 @@ pub struct Provisioners { members: BTreeMap, } +impl Provisioners { + pub fn iter(&self) -> impl Iterator { + self.members.iter() + } +} + #[derive(Clone, Debug)] pub struct ContextProvisioners { current: Provisioners, diff --git a/node/src/chain/acceptor.rs b/node/src/chain/acceptor.rs index c6f8643a2a..d83004446b 100644 --- a/node/src/chain/acceptor.rs +++ b/node/src/chain/acceptor.rs @@ -21,7 +21,7 @@ use node_data::message::Payload; use node_data::{Serializable, StepName}; use stake_contract_types::Unstake; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use std::time::Duration; use tokio::sync::RwLock; use tracing::{debug, info, warn}; @@ -101,6 +101,12 @@ impl ProvisionerChange { } } +pub static DUSK_KEY: LazyLock = LazyLock::new(|| { + let dusk_cpk_bytes = include_bytes!("../../../rusk/src/assets/dusk.cpk"); + PublicKey::try_from(*dusk_cpk_bytes) + .expect("Dusk consensus public key to be valid") +}); + impl Acceptor { /// Initializes a new `Acceptor` struct, /// @@ -269,7 +275,8 @@ impl Acceptor { .try_into() .map_err(|e| anyhow::anyhow!("Cannot deserialize bytes {e:?}"))?; let reward = ProvisionerChange::Reward(generator); - let mut changed_provisioners = vec![reward]; + let dusk_reward = ProvisionerChange::Reward(DUSK_KEY.clone()); + let mut changed_provisioners = vec![reward, dusk_reward]; // Update provisioners if a slash has been applied for bytes in blk.header().failed_iterations.to_missed_generators_bytes() @@ -463,9 +470,9 @@ impl Acceptor { let vm = self.vm.write().await; let txs = self.db.read().await.update(|t| { let (txs, verification_output) = if blk.is_final() { - vm.finalize(blk.inner())? + vm.finalize(blk.inner(), provisioners_list.current())? } else { - vm.accept(blk.inner())? + vm.accept(blk.inner(), provisioners_list.current())? }; assert_eq!(header.state_hash, verification_output.state_root); diff --git a/node/src/chain/consensus.rs b/node/src/chain/consensus.rs index 23061d73e4..ef6eea8a6d 100644 --- a/node/src/chain/consensus.rs +++ b/node/src/chain/consensus.rs @@ -321,10 +321,12 @@ impl Operations for Executor { let vm = self.vm.read().await; - Ok(vm.verify_state_transition(blk).map_err(|err| { - error!("failed to call VST {}", err); - Error::Failed - })?) + Ok(vm + .verify_state_transition(blk, self.provisioners.current()) + .map_err(|err| { + error!("failed to call VST {}", err); + Error::Failed + })?) } async fn execute_state_transition( @@ -340,9 +342,15 @@ impl Operations for Executor { let txs = view.get_txs_sorted_by_fee().map_err(|err| { anyhow::anyhow!("failed to get mempool txs: {}", err) })?; - let ret = vm.execute_state_transition(¶ms, txs).map_err( - |err| anyhow::anyhow!("failed to call EST {}", err), - )?; + let ret = vm + .execute_state_transition( + ¶ms, + txs, + self.provisioners.current(), + ) + .map_err(|err| { + anyhow::anyhow!("failed to call EST {}", err) + })?; Ok(ret) }) .map_err(|err: anyhow::Error| { diff --git a/node/src/lib.rs b/node/src/lib.rs index ea879515ac..9e0fdd014b 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -4,6 +4,8 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +#![feature(lazy_cell)] + pub mod chain; pub mod database; pub mod databroker; diff --git a/node/src/vm.rs b/node/src/vm.rs index 5c3c8d129c..8a4c7ff20e 100644 --- a/node/src/vm.rs +++ b/node/src/vm.rs @@ -19,6 +19,7 @@ pub trait VMExecution: Send + Sync + 'static { &self, params: &CallParams, txs: I, + provisioners: &Provisioners, ) -> anyhow::Result<( Vec, Vec, @@ -28,16 +29,19 @@ pub trait VMExecution: Send + Sync + 'static { fn verify_state_transition( &self, blk: &Block, + provisioners: &Provisioners, ) -> anyhow::Result; fn accept( &self, blk: &Block, + provisioners: &Provisioners, ) -> anyhow::Result<(Vec, VerificationOutput)>; fn finalize( &self, blk: &Block, + provisioners: &Provisioners, ) -> anyhow::Result<(Vec, VerificationOutput)>; fn preverify(&self, tx: &Transaction) -> anyhow::Result<()>; diff --git a/rusk/benches/block_ingestion.rs b/rusk/benches/block_ingestion.rs index f00c9ac8b0..09d7148a3f 100644 --- a/rusk/benches/block_ingestion.rs +++ b/rusk/benches/block_ingestion.rs @@ -27,6 +27,7 @@ use rand::SeedableRng; use tempfile::tempdir; use common::state::new_state; +use dusk_consensus::user::provisioners::Provisioners; fn load_txs() -> Vec { const TXS_BYTES: &[u8] = include_bytes!("block"); @@ -97,6 +98,7 @@ pub fn accept_benchmark(c: &mut Criterion) { txs, None, &[], + &Provisioners::empty(), ) .expect("Accepting transactions should succeed"); diff --git a/rusk/src/assets/stake_contract.wasm b/rusk/src/assets/stake_contract.wasm new file mode 100644 index 0000000000..8ace7f5158 Binary files /dev/null and b/rusk/src/assets/stake_contract.wasm differ diff --git a/rusk/src/bin/args.rs b/rusk/src/bin/args.rs index d07f57f48c..97b8e659b7 100644 --- a/rusk/src/bin/args.rs +++ b/rusk/src/bin/args.rs @@ -54,6 +54,10 @@ pub struct Args { /// path to encrypted BLS keys pub consensus_keys_path: Option, + #[clap(long)] + /// height at which migration will be performed + pub migration_height: Option, + #[clap(long)] /// Delay in milliseconds to mitigate UDP drops for DataBroker service in /// localnet diff --git a/rusk/src/bin/config/chain.rs b/rusk/src/bin/config/chain.rs index ede0786240..3ae2da1d21 100644 --- a/rusk/src/bin/config/chain.rs +++ b/rusk/src/bin/config/chain.rs @@ -14,6 +14,7 @@ use crate::args::Args; pub(crate) struct ChainConfig { db_path: Option, consensus_keys_path: Option, + migration_height: Option, } impl ChainConfig { @@ -27,6 +28,11 @@ impl ChainConfig { if let Some(db_path) = args.db_path.clone() { self.db_path = Some(db_path); } + + // Override config migration_height + if let Some(migration_height) = args.migration_height { + self.migration_height = Some(migration_height) + } } pub(crate) fn db_path(&self) -> PathBuf { @@ -52,4 +58,8 @@ impl ChainConfig { .display() .to_string() } + + pub(crate) fn migration_height(&self) -> Option { + self.migration_height + } } diff --git a/rusk/src/bin/main.rs b/rusk/src/bin/main.rs index c0f49d44fd..0f09809977 100644 --- a/rusk/src/bin/main.rs +++ b/rusk/src/bin/main.rs @@ -98,7 +98,7 @@ async fn main() -> Result<(), Box> { let (rusk, node, mut service_list) = { let state_dir = rusk_profile::get_rusk_state_dir()?; info!("Using state from {state_dir:?}"); - let rusk = Rusk::new(state_dir)?; + let rusk = Rusk::new(state_dir, config.chain.migration_height())?; info!("Rusk VM loaded"); diff --git a/rusk/src/lib/chain.rs b/rusk/src/lib/chain.rs index c39c4aeb29..9995bee983 100644 --- a/rusk/src/lib/chain.rs +++ b/rusk/src/lib/chain.rs @@ -30,6 +30,7 @@ pub struct Rusk { pub(crate) tip: Arc>, pub(crate) vm: Arc, dir: PathBuf, + migration_height: Option, } #[derive(Clone)] diff --git a/rusk/src/lib/chain/rusk.rs b/rusk/src/lib/chain/rusk.rs index 3c8744f286..c14077e9d5 100644 --- a/rusk/src/lib/chain/rusk.rs +++ b/rusk/src/lib/chain/rusk.rs @@ -11,12 +11,14 @@ use std::{fs, io}; use parking_lot::RwLock; use sha3::{Digest, Sha3_256}; use tokio::task; -use tracing::debug; +use tracing::{debug, error}; +use crate::chain::vm::migration::Migration; use dusk_bls12_381::BlsScalar; use dusk_bls12_381_sign::PublicKey as BlsPublicKey; use dusk_bytes::DeserializableSlice; use dusk_consensus::operations::VerificationOutput; +use dusk_consensus::user::provisioners::Provisioners; use node_data::ledger::{SpentTransaction, Transaction}; use phoenix_core::transaction::StakeData; use phoenix_core::Transaction as PhoenixTransaction; @@ -37,7 +39,10 @@ pub static DUSK_KEY: LazyLock = LazyLock::new(|| { }); impl Rusk { - pub fn new>(dir: P) -> Result { + pub fn new>( + dir: P, + migration_height: Option, + ) -> Result { let dir = dir.as_ref(); let commit_id_path = to_rusk_state_id_path(dir); @@ -66,6 +71,7 @@ impl Rusk { tip, vm, dir: dir.into(), + migration_height, }) } @@ -76,9 +82,21 @@ impl Rusk { generator: &BlsPublicKey, txs: I, missed_generators: &[BlsPublicKey], + provisioners: &Provisioners, ) -> Result<(Vec, Vec, VerificationOutput)> { - let mut session = self.session(block_height, None)?; + let session = self.session(block_height, None)?; + + let mut session = Migration::migrate( + self.migration_height, + session, + block_height, + provisioners, + ) + .map_err(|e| { + error!("Error while migrating: {e}"); + e + })?; let mut block_gas_left = block_gas_limit; @@ -163,17 +181,21 @@ impl Rusk { generator: &BlsPublicKey, txs: &[Transaction], missed_generators: &[BlsPublicKey], + provisioners: &Provisioners, ) -> Result<(Vec, VerificationOutput)> { - let mut session = self.session(block_height, None)?; + let session = self.session(block_height, None)?; accept( - &mut session, + session, block_height, block_gas_limit, generator, txs, missed_generators, + provisioners, + self.migration_height, ) + .map(|(a, b, _)| (a, b)) } /// Accept the given transactions. @@ -181,6 +203,7 @@ impl Rusk { /// * `consistency_check` - represents a state_root, the caller expects to /// be returned on successful transactions execution. Passing a None /// value disables the check. + #[allow(clippy::too_many_arguments)] pub fn accept_transactions( &self, block_height: u64, @@ -189,16 +212,19 @@ impl Rusk { txs: Vec, consistency_check: Option, missed_generators: &[BlsPublicKey], + provisioners: &Provisioners, ) -> Result<(Vec, VerificationOutput)> { - let mut session = self.session(block_height, None)?; + let session = self.session(block_height, None)?; - let (spent_txs, verification_output) = accept( - &mut session, + let (spent_txs, verification_output, session) = accept( + session, block_height, block_gas_limit, &generator, &txs[..], missed_generators, + provisioners, + self.migration_height, )?; if let Some(expected_verification) = consistency_check { @@ -219,6 +245,7 @@ impl Rusk { /// * `consistency_check` - represents a state_root, the caller expects to /// be returned on successful transactions execution. Passing None value /// disables the check. + #[allow(clippy::too_many_arguments)] pub fn finalize_transactions( &self, block_height: u64, @@ -227,16 +254,19 @@ impl Rusk { txs: Vec, consistency_check: Option, missed_generators: &[BlsPublicKey], + provisioners: &Provisioners, ) -> Result<(Vec, VerificationOutput)> { - let mut session = self.session(block_height, None)?; + let session = self.session(block_height, None)?; - let (spent_txs, verification_output) = accept( - &mut session, + let (spent_txs, verification_output, session) = accept( + session, block_height, block_gas_limit, &generator, &txs[..], missed_generators, + provisioners, + self.migration_height, )?; if let Some(expected_verification) = consistency_check { @@ -363,14 +393,28 @@ async fn delete_commits(vm: Arc, commits: Vec<[u8; 32]>) { } } +#[allow(clippy::too_many_arguments)] fn accept( - session: &mut Session, + session: Session, block_height: u64, block_gas_limit: u64, generator: &BlsPublicKey, txs: &[Transaction], missed_generators: &[BlsPublicKey], -) -> Result<(Vec, VerificationOutput)> { + provisioners: &Provisioners, + migration_height: Option, +) -> Result<(Vec, VerificationOutput, Session)> { + let mut session = Migration::migrate( + migration_height, + session, + block_height, + provisioners, + ) + .map_err(|e| { + error!("Error while migrating: {e}"); + e + })?; + let mut block_gas_left = block_gas_limit; let mut spent_txs = Vec::with_capacity(txs.len()); @@ -380,7 +424,7 @@ fn accept( for unspent_tx in txs { let tx = &unspent_tx.inner; - let receipt = execute(session, tx)?; + let receipt = execute(&mut session, tx)?; update_hasher(&mut event_hasher, &receipt.events); let gas_spent = receipt.gas_spent; @@ -400,7 +444,7 @@ fn accept( } reward_slash_and_update_root( - session, + &mut session, block_height, dusk_spent, generator, @@ -417,6 +461,7 @@ fn accept( state_root, event_hash, }, + session, )) } diff --git a/rusk/src/lib/chain/vm.rs b/rusk/src/lib/chain/vm.rs index 21c083e775..d5b204d6df 100644 --- a/rusk/src/lib/chain/vm.rs +++ b/rusk/src/lib/chain/vm.rs @@ -4,6 +4,7 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. +pub mod migration; mod query; use tracing::info; @@ -22,6 +23,7 @@ impl VMExecution for Rusk { &self, params: &CallParams, txs: I, + provisioners: &Provisioners, ) -> anyhow::Result<( Vec, Vec, @@ -36,6 +38,7 @@ impl VMExecution for Rusk { params.generator_pubkey.inner(), txs, ¶ms.missed_generators[..], + provisioners, ) .map_err(|inner| { anyhow::anyhow!("Cannot execute txs: {inner}!!") @@ -47,6 +50,7 @@ impl VMExecution for Rusk { fn verify_state_transition( &self, blk: &Block, + provisioners: &Provisioners, ) -> anyhow::Result { info!("Received verify_state_transition request"); let generator = blk.header().generator_bls_pubkey; @@ -61,6 +65,7 @@ impl VMExecution for Rusk { &generator, blk.txs(), &blk.header().failed_iterations.to_missed_generators()?, + provisioners, ) .map_err(|inner| anyhow::anyhow!("Cannot verify txs: {inner}!!"))?; @@ -70,6 +75,7 @@ impl VMExecution for Rusk { fn accept( &self, blk: &Block, + provisioners: &Provisioners, ) -> anyhow::Result<(Vec, VerificationOutput)> { info!("Received accept request"); let generator = blk.header().generator_bls_pubkey; @@ -88,6 +94,7 @@ impl VMExecution for Rusk { event_hash: blk.header().event_hash, }), &blk.header().failed_iterations.to_missed_generators()?, + provisioners, ) .map_err(|inner| anyhow::anyhow!("Cannot accept txs: {inner}!!"))?; @@ -97,6 +104,7 @@ impl VMExecution for Rusk { fn finalize( &self, blk: &Block, + provisioners: &Provisioners, ) -> anyhow::Result<(Vec, VerificationOutput)> { info!("Received finalize request"); let generator = blk.header().generator_bls_pubkey; @@ -115,6 +123,7 @@ impl VMExecution for Rusk { event_hash: blk.header().event_hash, }), &blk.header().failed_iterations.to_missed_generators()?, + provisioners, ) .map_err(|inner| { anyhow::anyhow!("Cannot finalize txs: {inner}!!") @@ -155,10 +164,9 @@ impl VMExecution for Rusk { let stake = self .provisioner(pk) .map_err(|e| anyhow::anyhow!("Cannot get provisioner {e}"))? - .and_then(|stake| { - stake.amount.map(|(value, eligibility)| { - Stake::new(value, stake.reward, eligibility) - }) + .map(|stake| { + let (value, eligibility) = stake.amount.unwrap_or_default(); + Stake::new(value, stake.reward, eligibility) }); Ok(stake) } @@ -197,12 +205,11 @@ impl Rusk { let provisioners = self .provisioners(base_commit) .map_err(|e| anyhow::anyhow!("Cannot get provisioners {e}"))? - .filter_map(|(key, stake)| { - stake.amount.map(|(value, eligibility)| { - let stake = Stake::new(value, stake.reward, eligibility); - let pubkey_bls = node_data::bls::PublicKey::new(key); - (pubkey_bls, stake) - }) + .map(|(key, stake)| { + let (value, eligibility) = stake.amount.unwrap_or_default(); + let stake = Stake::new(value, stake.reward, eligibility); + let pubkey_bls = node_data::bls::PublicKey::new(key); + (pubkey_bls, stake) }); let mut ret = Provisioners::empty(); for (pubkey_bls, stake) in provisioners { diff --git a/rusk/src/lib/chain/vm/migration.rs b/rusk/src/lib/chain/vm/migration.rs new file mode 100644 index 0000000000..3cc04ba534 --- /dev/null +++ b/rusk/src/lib/chain/vm/migration.rs @@ -0,0 +1,116 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use dusk_bls12_381_sign::PublicKey; +use dusk_consensus::user::provisioners::Provisioners; +use phoenix_core::transaction::StakeData; +use rusk_abi::{ContractData, ContractId, Error, Session, STAKE_CONTRACT}; +use std::io; +use std::sync::mpsc; +use std::time::Instant; +use tracing::info; + +const MIGRATION_GAS_LIMIT: u64 = 1_000_000_000; + +const NEW_STAKE_CONTRACT_BYTECODE: &[u8] = + include_bytes!("../../../assets/stake_contract.wasm"); + +pub struct Migration; + +impl Migration { + pub fn migrate( + migration_height: Option, + session: Session, + block_height: u64, + provisioners: &Provisioners, + ) -> crate::Result { + match migration_height { + Some(h) if h == block_height => (), + _ => return Ok(session), + } + info!("MIGRATING STAKE CONTRACT"); + let start = Instant::now(); + let mut session = session.migrate( + STAKE_CONTRACT, + NEW_STAKE_CONTRACT_BYTECODE, + ContractData::builder(), + MIGRATION_GAS_LIMIT, + |new_contract, session| { + Self::migrate_stakes(new_contract, session, provisioners) + }, + )?; + info!( + "MIGRATION FINISHED: {:?}", + Instant::now().duration_since(start) + ); + + info!("Performing sanity checks"); + + let start = Instant::now(); + let new_list = Self::query_provisioners(&mut session)?; + info!("Get new list: {:?}", Instant::now().duration_since(start)); + + let start = Instant::now(); + let old_list = Self::old_provisioners(provisioners); + + // Assert both new_list and provisioner_list are identical + if let Some((a, b)) = new_list.zip(old_list).find(|(a, b)| (a != b)) { + tracing::error!("new = {a:?}"); + tracing::error!("old = {b:?}"); + Err(io::Error::new(io::ErrorKind::Other, "Wrong migration"))?; + } + info!( + "Sanity checks OK: {:?}", + Instant::now().duration_since(start) + ); + + Ok(session) + } + + fn migrate_stakes( + new_contract: ContractId, + session: &mut Session, + provisioners: &Provisioners, + ) -> Result<(), Error> { + for (pk, stake_data) in Self::old_provisioners(provisioners) { + session.call::<_, ()>( + new_contract, + "insert_stake", + &(pk, stake_data.clone()), + MIGRATION_GAS_LIMIT, + )?; + } + Ok(()) + } + + fn query_provisioners( + session: &mut Session, + ) -> crate::Result> { + let (sender, receiver) = mpsc::channel(); + + session.feeder_call::<_, ()>(STAKE_CONTRACT, "stakes", &(), sender)?; + Ok(receiver.into_iter().map(|bytes| { + rkyv::from_bytes::<(PublicKey, StakeData)>(&bytes).expect( + "The contract should only return (pk, stake_data) tuples", + ) + })) + } + + fn old_provisioners( + provisioners: &Provisioners, + ) -> impl Iterator + '_ { + provisioners.iter().map(|(pk, stake)| { + ( + *pk.inner(), + StakeData { + amount: Some((stake.value(), stake.eligible_since)), + reward: stake.reward, + counter: stake.counter, + }, + ) + }) + } +} diff --git a/rusk/src/lib/http/rusk.rs b/rusk/src/lib/http/rusk.rs index fc3fd4cfd2..3551b1f74b 100644 --- a/rusk/src/lib/http/rusk.rs +++ b/rusk/src/lib/http/rusk.rs @@ -96,14 +96,15 @@ impl Rusk { let prov: Vec<_> = self .provisioners(None) .expect("Cannot query state for provisioners") - .filter_map(|(key, stake)| { + .map(|(key, stake)| { let key = bs58::encode(key.to_bytes()).into_string(); let (amount, eligibility) = stake.amount.unwrap_or_default(); - (amount > 0).then_some(Provisioner { + Provisioner { amount, eligibility, key, - }) + reward: stake.reward, + } }) .collect(); @@ -121,4 +122,5 @@ struct Provisioner { key: String, amount: u64, eligibility: u64, + reward: u64, } diff --git a/rusk/tests/common/state.rs b/rusk/tests/common/state.rs index 212344311e..fa74ce1a57 100644 --- a/rusk/tests/common/state.rs +++ b/rusk/tests/common/state.rs @@ -13,6 +13,7 @@ use rusk_recovery_tools::state::{self, Snapshot}; use dusk_bls12_381_sign::PublicKey; use dusk_consensus::operations::CallParams; +use dusk_consensus::user::provisioners::Provisioners; use dusk_wallet_core::Transaction as PhoenixTransaction; use node_data::{ bls::PublicKeyBytes, @@ -30,7 +31,7 @@ pub fn new_state>(dir: P, snapshot: &Snapshot) -> Result { let (_, commit_id) = state::deploy(dir, snapshot) .expect("Deploying initial state should succeed"); - let rusk = Rusk::new(dir).expect("Instantiating rusk should succeed"); + let rusk = Rusk::new(dir, None).expect("Instantiating rusk should succeed"); assert_eq!( commit_id, @@ -98,8 +99,12 @@ pub fn generator_procedure( missed_generators, }; - let (transfer_txs, discarded, execute_output) = - rusk.execute_state_transition(&call_params, txs.into_iter())?; + let (transfer_txs, discarded, execute_output) = rusk + .execute_state_transition( + &call_params, + txs.into_iter(), + &Provisioners::empty(), + )?; assert_eq!(transfer_txs.len(), expected.executed, "all txs accepted"); assert_eq!(discarded.len(), expected.discarded, "no discarded tx"); @@ -125,10 +130,12 @@ pub fn generator_procedure( ) .expect("valid block"); - let verify_output = rusk.verify_state_transition(&block)?; + let verify_output = + rusk.verify_state_transition(&block, &Provisioners::empty())?; info!("verify_state_transition new verification: {verify_output}",); - let (accept_txs, accept_output) = rusk.accept(&block)?; + let (accept_txs, accept_output) = + rusk.accept(&block, &Provisioners::empty())?; assert_eq!(accept_txs.len(), expected.executed, "all txs accepted"); diff --git a/rusk/tests/common/wallet.rs b/rusk/tests/common/wallet.rs index ff19413c56..4ba610544d 100644 --- a/rusk/tests/common/wallet.rs +++ b/rusk/tests/common/wallet.rs @@ -113,12 +113,11 @@ impl wallet::StateClient for TestStateClient { .ok_or(Error::OpeningPositionNotFound(*note.pos())) } - fn fetch_stake(&self, _pk: &PublicKey) -> Result { + fn fetch_stake(&self, pk: &PublicKey) -> Result { let stake = self .rusk - .provisioners(None)? - .find(|(pk, _)| pk == _pk) - .map(|(_, stake)| StakeInfo { + .provisioner(pk)? + .map(|stake| StakeInfo { amount: stake.amount, counter: stake.counter, reward: stake.reward,