Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests(app): 🎇 mock consensus delegates, and undelegates, to a validator #4044

Merged
merged 3 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 265 additions & 5 deletions crates/core/app/tests/mock_consensus_staking.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod common;

use {
self::common::BuilderExt,
anyhow::Context,
Expand All @@ -18,8 +20,9 @@ use {
tracing::{error_span, info, Instrument},
};

mod common;

/// The length of the [`penumbra_sct`] epoch.
///
/// This test relies on many epochs turning over, so we will work with a shorter epoch duration.
const EPOCH_DURATION: u64 = 8;

#[tokio::test]
Expand Down Expand Up @@ -49,15 +52,15 @@ async fn mock_consensus_can_define_and_delegate_to_a_validator() -> anyhow::Resu
}?;

// Sync the mock client, using the test wallet's spend key, to the latest snapshot.
let client = MockClient::new(test_keys::SPEND_KEY.clone())
let mut client = MockClient::new(test_keys::SPEND_KEY.clone())
.with_sync_to_storage(&storage)
.await?
.tap(|c| info!(client.notes = %c.notes.len(), "mock client synced to test storage"));

// Fast forward to the next epoch.
let snapshot_start = storage.latest_snapshot();
node.fast_forward(EPOCH_DURATION)
.instrument(error_span!("fast forwarding test node to next epoch"))
.instrument(error_span!("fast forwarding test node to second epoch"))
.await
.context("fast forwarding {EPOCH_LENGTH} blocks")?;
let snapshot_end = storage.latest_snapshot();
Expand Down Expand Up @@ -170,13 +173,20 @@ async fn mock_consensus_can_define_and_delegate_to_a_validator() -> anyhow::Resu
let tx = client.witness_auth_build(&plan).await?;

// Execute the transaction, applying it to the chain state.
node.block().add_tx(tx.encode_to_vec()).execute().await?;
node.block()
.add_tx(tx.encode_to_vec())
.execute()
.instrument(error_span!(
"executing block with validator definition transaction"
))
.await?;
let post_tx_snapshot = storage.latest_snapshot();

// Show that the set of validators looks correct.
{
use penumbra_stake::{component::ConsensusIndexRead, validator::State};
let snapshot = post_tx_snapshot;
info!("checking consensus set in block after validator definition");
// The original validator should still be active.
assert_eq!(
snapshot.get_validator_state(&existing_validator_id).await?,
Expand All @@ -198,6 +208,256 @@ async fn mock_consensus_can_define_and_delegate_to_a_validator() -> anyhow::Resu
);
}

// Now, create a transaction that delegates to the new validator.
let plan = {
use {
penumbra_asset::STAKING_TOKEN_ASSET_ID,
penumbra_sct::component::clock::EpochRead,
penumbra_shielded_pool::{OutputPlan, SpendPlan},
penumbra_transaction::{
memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan,
},
};
let snapshot = storage.latest_snapshot();
client.sync_to_latest(snapshot.clone()).await?;
let rate = snapshot
.get_validator_rate(&new_validator_id)
.await?
.ok_or(anyhow::anyhow!("new validator has a rate"))?
.tap(|rate| tracing::info!(?rate, "got new validator rate"));
let note = client
.notes
.values()
.filter(|n| n.asset_id() == *STAKING_TOKEN_ASSET_ID)
.cloned()
.next()
.expect("the test account should have one staking token note");
let spend = SpendPlan::new(
&mut rand_core::OsRng,
note.clone(),
client
.position(note.commit())
.expect("note should be in mock client's tree"),
);
let delegate = rate.build_delegate(
storage.latest_snapshot().get_current_epoch().await?,
note.amount(),
);
let output = OutputPlan::new(
&mut rand_core::OsRng,
delegate.delegation_value(),
*test_keys::ADDRESS_1,
);
let mut plan = TransactionPlan {
actions: vec![spend.into(), output.into(), delegate.into()],
// Now fill out the remaining parts of the transaction needed for verification:
memo: MemoPlan::new(&mut OsRng, MemoPlaintext::blank_memo(*test_keys::ADDRESS_0))
.map(Some)?,
detection_data: None, // We'll set this automatically below
transaction_parameters: TransactionParameters {
chain_id: TestNode::<()>::CHAIN_ID.to_string(),
..Default::default()
},
};
plan.populate_detection_data(rand_core::OsRng, 0);
plan
};
let tx = client.witness_auth_build(&plan).await?;

// Execute the transaction, applying it to the chain state.
node.block()
.add_tx(tx.encode_to_vec())
.execute()
.instrument(error_span!("executing block with delegation transaction"))
.await?;
let post_delegate_snapshot = storage.latest_snapshot();

// Show that the set of validators still looks correct. We should not see any changes yet.
{
use penumbra_stake::{component::ConsensusIndexRead, validator::State};
let snapshot = post_delegate_snapshot;
info!("checking consensus set in block after delegation");
// The original validator should still be active.
assert_eq!(
snapshot.get_validator_state(&existing_validator_id).await?,
Some(State::Active),
"validator should be active"
);
// The new validator should be defined, but not yet active. It should not be inclueded in
// consensus yet.
assert_eq!(
snapshot.get_validator_state(&new_validator_id).await?,
Some(State::Defined),
"new validator definition should be defined but not active"
);
// The original validator should still be the only validator in the consensus set.
assert_eq!(
snapshot.get_consensus_set().await?.len(),
1,
"the new validator should not be part of the consensus set yet"
);
}

// Fast forward to the next epoch.
node.fast_forward(EPOCH_DURATION)
.instrument(error_span!(
"fast forwarding test node to epoch after delegation"
))
.await
.context("fast forwarding {EPOCH_LENGTH} blocks")?;
let post_delegate_next_epoch_snapshot = storage.latest_snapshot();

// Show that now, after an epoch and with a delegation, the validator is marked active.
{
use penumbra_stake::{component::ConsensusIndexRead, validator::State};
info!("checking consensus set in epoch after delegation");
let snapshot = post_delegate_next_epoch_snapshot;
// The original validator should still be active.
assert_eq!(
snapshot.get_validator_state(&existing_validator_id).await?,
Some(State::Active),
"validator should be active"
);
// The new validator should now be active.
assert_eq!(
snapshot.get_validator_state(&new_validator_id).await?,
Some(State::Active),
"new validator should be active"
);
// There should now be two validators in the consensus set.
assert_eq!(
snapshot.get_consensus_set().await?.len(),
2,
"the new validator should now be part of the consensus set"
);
}

// Build a transaction that will now undelegate from the validator.
let plan = {
use {
penumbra_sct::component::clock::EpochRead,
penumbra_shielded_pool::{OutputPlan, SpendPlan},
penumbra_stake::DelegationToken,
penumbra_transaction::{
memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan,
},
};
let snapshot = storage.latest_snapshot();
client.sync_to_latest(snapshot.clone()).await?;
let rate = snapshot
.get_validator_rate(&new_validator_id)
.await?
.ok_or(anyhow::anyhow!("new validator has a rate"))?
.tap(|rate| tracing::info!(?rate, "got new validator rate"));

let undelegation_id = DelegationToken::new(new_validator_id).id();
let note = client
.notes
.values()
.filter(|n| n.asset_id() == undelegation_id)
.cloned()
.next()
.expect("the test account should have one staking token note");
let spend = SpendPlan::new(
&mut rand_core::OsRng,
note.clone(),
client
.position(note.commit())
.expect("note should be in mock client's tree"),
);
let undelegate = rate.build_undelegate(
storage.latest_snapshot().get_current_epoch().await?,
note.amount(),
);
let output = OutputPlan::new(
&mut rand_core::OsRng,
undelegate.unbonded_value(),
*test_keys::ADDRESS_1,
);

let mut plan = TransactionPlan {
actions: vec![spend.into(), output.into(), undelegate.into()],
// Now fill out the remaining parts of the transaction needed for verification:
memo: MemoPlan::new(&mut OsRng, MemoPlaintext::blank_memo(*test_keys::ADDRESS_0))
.map(Some)?,
detection_data: None, // We'll set this automatically below
transaction_parameters: TransactionParameters {
chain_id: TestNode::<()>::CHAIN_ID.to_string(),
..Default::default()
},
};
plan.populate_detection_data(rand_core::OsRng, 0);
plan
};
let tx = client.witness_auth_build(&plan).await?;

// Execute the transaction, applying it to the chain state.
node.block()
.add_tx(tx.encode_to_vec())
.execute()
.instrument(error_span!("executing block with undelegation transaction"))
.await?;
let post_undelegate_snapshot = storage.latest_snapshot();

// Show that the consensus set has not changed yet.
{
use penumbra_stake::{component::ConsensusIndexRead, validator::State};
let snapshot = post_undelegate_snapshot;
info!("checking consensus set in block after undelegation");
// The original validator should still be active.
assert_eq!(
snapshot.get_validator_state(&existing_validator_id).await?,
Some(State::Active),
"validator should be active"
);
// The new validator should now be active.
assert_eq!(
snapshot.get_validator_state(&new_validator_id).await?,
Some(State::Active),
"new validator should be active"
);
// There should now be two validators in the consensus set.
assert_eq!(
snapshot.get_consensus_set().await?.len(),
2,
"the new validator should now be part of the consensus set"
);
}

// Fast forward to the next epoch.
node.fast_forward(EPOCH_DURATION)
.instrument(error_span!(
"fast forwarding test node to epoch after undelegation"
))
.await
.context("fast forwarding {EPOCH_LENGTH} blocks")?;
let post_undelegate_next_epoch_snapshot = storage.latest_snapshot();

// Show that after undelegating, the validator is no longer marked active.
{
use penumbra_stake::{component::ConsensusIndexRead, validator::State};
info!("checking consensus set in epoch after undelegation");
let snapshot = post_undelegate_next_epoch_snapshot;
// The original validator should still be active.
assert_eq!(
snapshot.get_validator_state(&existing_validator_id).await?,
Some(State::Active),
"validator should be active"
);
// The new validator should now have reverted to be defined. It no longer has enough
// delegated stake to participate in consensus.
assert_eq!(
snapshot.get_validator_state(&new_validator_id).await?,
Some(State::Defined),
"new validator definition should be defined but not active"
);
assert_eq!(
snapshot.get_consensus_set().await?.len(),
1,
"the new validator should not be part of the consensus set yet"
);
}

// The test passed. Free our temporary storage and drop our tracing subscriber.
Ok(())
.tap(|_| drop(node))
Expand Down
25 changes: 17 additions & 8 deletions crates/core/component/stake/src/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,27 @@ impl EffectingData for Delegate {
impl Delegate {
/// Return the balance resulting from issuing delegation tokens from staking tokens.
pub fn balance(&self) -> Balance {
let stake = Balance::from(Value {
amount: self.unbonded_amount,
asset_id: STAKING_TOKEN_ASSET_ID.clone(),
});
let stake: Balance = self.unbonded_value().into();
let delegation: Balance = self.delegation_value().into();

let delegation = Balance::from(Value {
// We produce the delegation tokens and consume the staking tokens.
delegation - stake
}

/// Returns the [`Value`] of the delegation [`Amount`].
pub fn delegation_value(&self) -> Value {
Value {
amount: self.delegation_amount,
asset_id: DelegationToken::new(self.validator_identity.clone()).id(),
});
}
}

// We produce the delegation tokens and consume the staking tokens.
delegation - stake
/// Returns the [`Value`] of the unbonded [`Amount`].
pub fn unbonded_value(&self) -> Value {
Value {
amount: self.unbonded_amount,
asset_id: STAKING_TOKEN_ASSET_ID.clone(),
}
}
}

Expand Down
27 changes: 18 additions & 9 deletions crates/core/component/stake/src/undelegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,8 @@ impl EffectingData for Undelegate {
impl Undelegate {
/// Return the balance after consuming delegation tokens, and producing unbonding tokens.
pub fn balance(&self) -> Balance {
let stake = Balance::from(Value {
amount: self.unbonded_amount,
asset_id: self.unbonding_token().id(),
});

let delegation = Balance::from(Value {
amount: self.delegation_amount,
asset_id: self.delegation_token().id(),
});
let stake: Balance = self.unbonded_value().into();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally subjective and lgtm, but I have come to appreciate struct literals because they make it immediately apparent what data we are constructing vs. using constructors that add a level of indirection.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree! this came out of a pairing session with henry, because want to use those same literals in the mock_consensus_staking test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let delegation: Balance = self.delegation_value().into();

// We consume the delegation tokens and produce the staking tokens.
stake - delegation
cratelyn marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -57,9 +50,25 @@ impl Undelegate {
)
}

/// Returns the [`Value`] of the unbonded [`Amount`].
pub fn unbonded_value(&self) -> Value {
Value {
amount: self.unbonded_amount,
asset_id: self.unbonding_token().id(),
}
}

pub fn delegation_token(&self) -> DelegationToken {
DelegationToken::new(self.validator_identity.clone())
}

/// Returns the [`Value`] of the delegation [`Amount`].
pub fn delegation_value(&self) -> Value {
Value {
amount: self.delegation_amount,
asset_id: self.delegation_token().id(),
}
}
}

impl DomainType for Undelegate {
Expand Down
Loading