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

app: 💸 a mock consensus spend is performed #3891

Merged
merged 4 commits into from
Feb 27, 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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions crates/core/app/src/action_handler/transaction/stateful.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ pub async fn fmd_parameters_valid<S: StateRead>(state: S, transaction: &Transact
)
}

#[tracing::instrument(
skip_all,
fields(
current_fmd.precision_bits = current_fmd_parameters.precision_bits,
previous_fmd.precision_bits = previous_fmd_parameters.precision_bits,
previous_fmd.as_of_block_height = previous_fmd_parameters.as_of_block_height,
block_height,
)
)]
pub fn fmd_precision_within_grace_period(
tx: &Transaction,
previous_fmd_parameters: fmd::Parameters,
Expand All @@ -42,13 +51,21 @@ pub fn fmd_precision_within_grace_period(
{
// Clue must be using the current `fmd::Parameters`, or be within
// `FMD_GRACE_PERIOD_BLOCKS` of the previous `fmd::Parameters`.
if clue.precision_bits() == current_fmd_parameters.precision_bits
|| (clue.precision_bits() == previous_fmd_parameters.precision_bits
&& block_height
< previous_fmd_parameters.as_of_block_height + FMD_GRACE_PERIOD_BLOCKS)
{
let clue_precision = clue.precision_bits();
let using_current_precision = clue_precision == current_fmd_parameters.precision_bits;
let using_previous_precision = clue_precision == previous_fmd_parameters.precision_bits;
let within_grace_period =
block_height < previous_fmd_parameters.as_of_block_height + FMD_GRACE_PERIOD_BLOCKS;
if using_current_precision || (using_previous_precision && within_grace_period) {
continue;
} else {
tracing::error!(
%clue_precision,
%using_current_precision,
%using_previous_precision,
%within_grace_period,
"invalid clue precision"
);
anyhow::bail!("consensus rule violated: invalid clue precision");
}
}
Expand Down
112 changes: 72 additions & 40 deletions crates/core/app/tests/mock_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ use {
cnidarium::TempStorage,
penumbra_keys::test_keys,
penumbra_mock_client::MockClient,
penumbra_mock_consensus::TestNode,
penumbra_proto::DomainType,
penumbra_sct::component::clock::EpochRead,
penumbra_shielded_pool::SpendPlan,
penumbra_transaction::TransactionPlan,
std::ops::Deref,
penumbra_sct::component::{clock::EpochRead, tree::SctRead as _},
penumbra_shielded_pool::{OutputPlan, SpendPlan},
penumbra_transaction::{
memo::MemoPlaintext, plan::MemoPlan, TransactionParameters, TransactionPlan,
},
rand_core::OsRng,
tap::Tap,
tracing::{error_span, Instrument},
tracing::{error_span, info, Instrument},
};

/// Exercises that a test node can be instantiated using the consensus service.
Expand Down Expand Up @@ -75,59 +78,88 @@ async fn mock_consensus_can_send_a_sequence_of_empty_blocks() -> anyhow::Result<
}

#[tokio::test]
async fn mock_consensus_can_send_a_spend_action() -> anyhow::Result<()> {
async fn mock_consensus_can_spend_notes_and_detect_outputs() -> anyhow::Result<()> {
// Install a test logger, acquire some temporary storage, and start the test node.
let guard = common::set_tracing_subscriber();
let storage = TempStorage::new().await?;
let mut test_node = common::start_test_node(&storage).await?;
let mut rng = <rand_chacha::ChaChaRng as rand_core::SeedableRng>::seed_from_u64(0xBEEF);

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

// Take one of the test account's notes...
let (commitment, note) = client
// Take one of the test wallet's notes, and send it to a different account.
let input_note = client
.notes
.iter()
.values()
.cloned()
.next()
.ok_or_else(|| anyhow!("mock client had no note"))?
.tap(|(commitment, note)| {
tracing::info!(?commitment, ?note, "mock client note commitment")
});

// Build a transaction spending this note.
let tx = {
let position = client
.sct
.witness(*commitment)
.ok_or_else(|| anyhow!("commitment is not witnessed"))?
.position();
let spend = SpendPlan::new(&mut rng, note.clone(), position);
let plan = TransactionPlan {
actions: vec![spend.into()],
.ok_or_else(|| anyhow!("mock client had no note"))?;

// Write down a transaction plan with exactly one spend and one output.
let mut plan = TransactionPlan {
actions: vec![
// First, spend the selected input note.
SpendPlan::new(
&mut OsRng,
input_note.clone(),
// Spends require _positioned_ notes, in order to compute their nullifiers.
client
.position(input_note.commit())
.ok_or_else(|| anyhow!("input note commitment was unknown to mock client"))?,
)
.into(),
// Next, create a new output of the exact same amount.
OutputPlan::new(&mut OsRng, input_note.value(), *test_keys::ADDRESS_1).into(),
],
// Now fill out the remaining parts of the transaction needed for verification:
memo: Some(MemoPlan::new(
&mut OsRng,
MemoPlaintext::blank_memo(*test_keys::ADDRESS_0),
)?),
detection_data: None, // We'll set this automatically below
transaction_parameters: TransactionParameters {
chain_id: TestNode::<()>::CHAIN_ID.to_string(),
..Default::default()
};
let witness = plan.witness_data(&client.sct)?;
let auth = plan.authorize(rand_core::OsRng, spend_key)?;
plan.build_concurrent(viewing_key, &witness, &auth).await?
},
};
plan.populate_detection_data(OsRng, 0);

let tx = client.witness_auth_build(&plan).await?;

// Execute the transaction, and sync another mock client up to the latest snapshot.
// Execute the transaction, applying it to the chain state.
let pre_tx_snapshot = storage.latest_snapshot();
test_node
.block()
.with_data(vec![tx.encode_to_vec()]) // TODO(kate): add a `with_tx` extension method
.with_data(vec![tx.encode_to_vec()])
.execute()
.await?;
let post_tx_snapshot = storage.latest_snapshot();

// Sync to the latest storage snapshot once more.
let client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone())
.with_sync_to_storage(&storage)
.await?;
// Check that the nullifiers were spent as a result of the transaction:
for nf in tx.spent_nullifiers() {
assert!(pre_tx_snapshot.spend_info(nf).await?.is_none());
assert!(post_tx_snapshot.spend_info(nf).await?.is_some());
}

// Sync the client up to the current block
client.sync_to_latest(post_tx_snapshot).await?;

client.notes.get(&commitment).unwrap();
// Check that the client was able to detect the new note:

// Grab the output note we're expecting to see...
let output_nc = tx
.outputs()
.next()
.expect("tx has one output")
.body
.note_payload
.note_commitment
.clone();
// ... and check that it's now in the client's note set.
assert!(client.notes.contains_key(&output_nc));

// Free our temporary storage.
drop(storage);
Expand Down
8 changes: 4 additions & 4 deletions crates/core/app/tests/spend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async fn spend_happy_path() -> anyhow::Result<()> {
let height = 1;

// Precondition: This test uses the default genesis which has existing notes for the test keys.
let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone());
let mut client = MockClient::new(test_keys::SPEND_KEY.clone());
let sk = test_keys::SPEND_KEY.clone();
client.sync_to(0, state.deref()).await?;
let note = client.notes.values().next().unwrap().clone();
Expand Down Expand Up @@ -112,7 +112,7 @@ async fn invalid_dummy_spend() {
let height = 1;

// Precondition: This test uses the default genesis which has existing notes for the test keys.
let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone());
let mut client = MockClient::new(test_keys::SPEND_KEY.clone());
let sk = test_keys::SPEND_KEY.clone();
client.sync_to(0, state.deref()).await.unwrap();
let note = client.notes.values().next().unwrap().clone();
Expand Down Expand Up @@ -212,7 +212,7 @@ async fn spend_duplicate_nullifier_previous_transaction() {
let height = 1;

// Precondition: This test uses the default genesis which has existing notes for the test keys.
let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone());
let mut client = MockClient::new(test_keys::SPEND_KEY.clone());
let sk = test_keys::SPEND_KEY.clone();
client
.sync_to(0, state.deref())
Expand Down Expand Up @@ -303,7 +303,7 @@ async fn spend_duplicate_nullifier_same_transaction() {
let height = 1;

// Precondition: This test uses the default genesis which has existing notes for the test keys.
let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone());
let mut client = MockClient::new(test_keys::SPEND_KEY.clone());
let sk = test_keys::SPEND_KEY.clone();
client
.sync_to(0, state.deref())
Expand Down
8 changes: 4 additions & 4 deletions crates/core/app/tests/swap_and_swap_claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ async fn swap_and_swap_claim() -> anyhow::Result<()> {
// means we have to synchronize a client's view of the test chain's SCT
// state.
let epoch_duration = state.get_epoch_duration_parameter().await?;
let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone());
let mut client = MockClient::new(test_keys::SPEND_KEY.clone());
// TODO: generalize StateRead/StateWrite impls from impl for &S to impl for Deref<Target=S>
client.sync_to(1, state.deref()).await?;

let output_data = state.output_data(height, trading_pair).await?.unwrap();

let commitment = swap.body.payload.commitment;
let swap_auth_path = client.witness(commitment).unwrap();
let swap_auth_path = client.witness_commitment(commitment).unwrap();
let detected_plaintext = client.swap_by_commitment(&commitment).unwrap();
assert_eq!(plaintext, detected_plaintext);

Expand Down Expand Up @@ -207,7 +207,7 @@ async fn swap_claim_duplicate_nullifier_previous_transaction() {

// 6. Create a SwapClaim action
let epoch_duration = state.get_epoch_duration_parameter().await.unwrap();
let mut client = MockClient::new(test_keys::FULL_VIEWING_KEY.clone());
let mut client = MockClient::new(test_keys::SPEND_KEY.clone());
client.sync_to(1, state.deref()).await.unwrap();

let output_data = state
Expand All @@ -217,7 +217,7 @@ async fn swap_claim_duplicate_nullifier_previous_transaction() {
.unwrap();

let commitment = swap.body.payload.commitment;
let swap_auth_path = client.witness(commitment).unwrap();
let swap_auth_path = client.witness_commitment(commitment).unwrap();
let detected_plaintext = client.swap_by_commitment(&commitment).unwrap();
assert_eq!(plaintext, detected_plaintext);

Expand Down
4 changes: 2 additions & 2 deletions crates/core/transaction/src/memo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ impl MemoPlaintext {
self.into()
}

pub fn blank_memo(address: Address) -> MemoPlaintext {
pub fn blank_memo(return_address: Address) -> MemoPlaintext {
MemoPlaintext {
return_address: address,
return_address,
text: String::new(),
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/test/mock-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ penumbra-shielded-pool = {workspace = true, features = [
"component",
], default-features = true}
penumbra-tct = {workspace = true, default-features = true}
penumbra-transaction = {workspace = true, default-features = true}
rand_core = {workspace = true}

64 changes: 56 additions & 8 deletions crates/test/mock-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
use anyhow::Error;
use cnidarium::StateRead;
use penumbra_compact_block::{component::StateReadExt as _, CompactBlock, StatePayload};
use penumbra_dex::swap::SwapPlaintext;
use penumbra_keys::FullViewingKey;
use penumbra_keys::{keys::SpendKey, FullViewingKey};
use penumbra_sct::component::{clock::EpochRead, tree::SctRead};
use penumbra_shielded_pool::{note, Note};
use penumbra_tct as tct;
use penumbra_transaction::{AuthorizationData, Transaction, TransactionPlan, WitnessData};
use rand_core::OsRng;
use std::collections::BTreeMap;

/// A bare-bones mock client for use exercising the state machine.
pub struct MockClient {
latest_height: u64,
sk: SpendKey,
pub fvk: FullViewingKey,
pub notes: BTreeMap<note::StateCommitment, Note>,
swaps: BTreeMap<tct::StateCommitment, SwapPlaintext>,
pub sct: penumbra_tct::Tree,
}

impl MockClient {
pub fn new(fvk: FullViewingKey) -> MockClient {
pub fn new(sk: SpendKey) -> MockClient {
Self {
latest_height: u64::MAX,
fvk,
fvk: sk.full_viewing_key().clone(),
sk,
notes: Default::default(),
sct: Default::default(),
swaps: Default::default(),
Expand All @@ -32,19 +37,24 @@ impl MockClient {
storage: impl AsRef<cnidarium::Storage>,
) -> anyhow::Result<Self> {
let latest = storage.as_ref().latest_snapshot();
let height = latest.get_block_height().await?;
let state = cnidarium::StateDelta::new(latest);
self.sync_to(height, state).await?;
self.sync_to_latest(latest).await?;

Ok(self)
}

pub async fn sync_to_latest<R: StateRead>(&mut self, state: R) -> anyhow::Result<()> {
let height = state.get_block_height().await?;
self.sync_to(height, state).await?;
Ok(())
}

pub async fn sync_to<R: StateRead>(
&mut self,
target_height: u64,
state: R,
) -> anyhow::Result<()> {
for height in 0..=target_height {
let start_height = self.latest_height.wrapping_add(1);
for height in start_height..=target_height {
let compact_block = state
.compact_block(height)
.await?
Expand Down Expand Up @@ -149,7 +159,45 @@ impl MockClient {
self.swaps.get(commitment).cloned()
}

pub fn witness(&self, commitment: note::StateCommitment) -> Option<penumbra_tct::Proof> {
pub fn position(&self, commitment: note::StateCommitment) -> Option<penumbra_tct::Position> {
self.sct.witness(commitment).map(|proof| proof.position())
}

pub fn witness_commitment(
&self,
commitment: note::StateCommitment,
) -> Option<penumbra_tct::Proof> {
self.sct.witness(commitment)
}

pub fn witness_plan(&self, plan: &TransactionPlan) -> Result<WitnessData, Error> {
Ok(WitnessData {
anchor: self.sct.root(),
// TODO: this will only witness spends, not other proofs like swaps
state_commitment_proofs: plan
.spend_plans()
.map(|spend| {
let nc = spend.note.commit();
Ok((
nc,
self.sct.witness(nc).ok_or_else(|| {
anyhow::anyhow!("note commitment {:?} unknown to client", nc)
})?,
))
})
.collect::<Result<_, Error>>()?,
})
}

pub fn authorize_plan(&self, plan: &TransactionPlan) -> Result<AuthorizationData, Error> {
plan.authorize(OsRng, &self.sk)
}

pub async fn witness_auth_build(&self, plan: &TransactionPlan) -> Result<Transaction, Error> {
let witness_data = self.witness_plan(plan)?;
let auth_data = self.authorize_plan(plan)?;
plan.clone()
.build_concurrent(&self.fvk, &witness_data, &auth_data)
.await
}
}
Loading
Loading