Skip to content

Commit

Permalink
Basic IBC Handshake Test (#4797)
Browse files Browse the repository at this point in the history
## Describe your changes

This adds a basic IBC handshake test, using the existing mock client.
Some opportunistic refactoring is also included.

This PR adds a new `tests::common::ibc_tests` module, which contains a
`MockRelayer` that can be extended later on.

Follow-up tasks should be basic transfer testing, transfer timeout
testing, and testing with malformed requests.

While debugging this test, bugs were found in the various IBC query
APIs, specifically that the `proof_height` was consistently being
returned one lower than the height whose header would contain the
app_hash necessary for validating the proof. The Go relayer is
unaffected because it uses the ABCI RPC query interface instead, and
Hermes uses the affected APIs but discards the affected `proof_height`
fields and uses its own internal mechanisms for height tracking instead.
Fixes were included for the affected APIs.

## Issue

Closes #3758

## Checklist before requesting a review

- [ ] If this code contains consensus-breaking changes, I have added the
"consensus-breaking" label. Otherwise, I declare my belief that there
are not consensus-breaking changes, for the following reason:

> There are changes to TendermintProxy and IBC RPC responses however
anyone using the affected RPCs would quickly run into the issues we saw
in testing that revealed the bugs in the RPC responses, and the chain
would properly block any requests made based on the incorrect response
values. The changeset also affects the test code heavily however there
should be nothing that affects consensus or state.

---------

Co-authored-by: Ava Howell <[email protected]>
  • Loading branch information
zbuc and avahowell committed Aug 22, 2024
1 parent 87adc8d commit 8ee733c
Show file tree
Hide file tree
Showing 15 changed files with 1,762 additions and 122 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

44 changes: 43 additions & 1 deletion crates/bin/pd/src/network/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,48 @@ pub struct ValidatorKeys {
}

impl ValidatorKeys {
/// Use a hard-coded seed to generate a new set of validator keys.
pub fn from_seed(seed: [u8; 32]) -> Self {
// Create the spend key for this node.
let seed = SpendKeyBytes(seed);
let spend_key = SpendKey::from(seed.clone());

// Create signing key and verification key for this node.
let validator_id_sk = spend_key.spend_auth_key();
let validator_id_vk = VerificationKey::from(validator_id_sk);

let validator_cons_sk = ed25519_consensus::SigningKey::new(OsRng);

// generate consensus key for tendermint.
let validator_cons_sk = tendermint::PrivateKey::Ed25519(
validator_cons_sk
.as_bytes()
.as_slice()
.try_into()
.expect("32 bytes"),
);
let validator_cons_pk = validator_cons_sk.public_key();

// generate P2P auth key for tendermint.
let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
let signing_key_bytes = node_key_sk.as_bytes().as_slice();

// generate consensus key for tendermint.
let node_key_sk =
tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes"));
let node_key_pk = node_key_sk.public_key();

ValidatorKeys {
validator_id_sk: validator_id_sk.clone(),
validator_id_vk,
validator_cons_sk,
validator_cons_pk,
node_key_sk,
node_key_pk,
validator_spend_key: seed,
}
}

pub fn generate() -> Self {
// Create the spend key for this node.
// TODO: change to use seed phrase
Expand All @@ -198,7 +240,7 @@ impl ValidatorKeys {
let validator_cons_pk = validator_cons_sk.public_key();

// generate P2P auth key for tendermint.
let node_key_sk = ed25519_consensus::SigningKey::new(OsRng);
let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
let signing_key_bytes = node_key_sk.as_bytes().as_slice();

// generate consensus key for tendermint.
Expand Down
1 change: 1 addition & 0 deletions crates/core/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ rand_chacha = { workspace = true }
rand_core = { workspace = true }
tap = { workspace = true }
tempfile = { workspace = true }
tendermint-config = { workspace = true }
tower-http = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
Expand Down
6 changes: 4 additions & 2 deletions crates/core/app/src/server/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ impl Consensus {
&mut self,
proposal: request::ProcessProposal,
) -> Result<response::ProcessProposal> {
tracing::info!(height = ?proposal.height, proposer = ?proposal.proposer_address, hash = %proposal.hash, "processing proposal");
// We process the propopsal in an isolated state fork. Eventually, we should cache this work and
tracing::info!(height = ?proposal.height, proposer = ?proposal.proposer_address, proposal_hash = %proposal.hash, "processing proposal");
// We process the proposal in an isolated state fork. Eventually, we should cache this work and
// re-use it when processing a `FinalizeBlock` message (starting in `0.38.x`).
let mut tmp_app = App::new(self.storage.latest_snapshot());
Ok(tmp_app.process_proposal(proposal).await)
Expand All @@ -187,7 +187,9 @@ impl Consensus {
// We don't need to print the block height, because it will already be
// included in the span modeling the abci request handling.
tracing::info!(time = ?begin_block.header.time, "beginning block");

let events = self.app.begin_block(&begin_block).await;

Ok(response::BeginBlock { events })
}

Expand Down
193 changes: 193 additions & 0 deletions crates/core/app/tests/common/ibc_tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use {anyhow::Result, std::time::Duration};

mod relayer;
use anyhow::Context as _;
use decaf377_rdsa::{SigningKey, SpendAuth, VerificationKey};
use penumbra_app::{
app::{MAX_BLOCK_TXS_PAYLOAD_BYTES, MAX_EVIDENCE_SIZE_BYTES},
genesis,
};
use penumbra_keys::keys::{SpendKey, SpendKeyBytes};
use penumbra_mock_consensus::TestNode;
use penumbra_proto::core::component::stake::v1::Validator;
use penumbra_shielded_pool::genesis::Allocation;
use penumbra_stake::{DelegationToken, GovernanceKey, IdentityKey};
#[allow(unused_imports)]
pub use relayer::MockRelayer;

mod node;
pub use node::TestNodeWithIBC;
use serde::Deserialize;
use tendermint::{consensus::params::AbciParams, public_key::Algorithm, Genesis};

/// Collection of all keypairs required for a Penumbra validator.
/// Used to generate a stable identity for a [`NetworkValidator`].
/// TODO: copied this from pd crate
#[derive(Deserialize)]
pub struct ValidatorKeys {
/// Penumbra spending key and viewing key for this node.
/// These need to be real curve points.
pub validator_id_sk: SigningKey<SpendAuth>,
pub validator_id_vk: VerificationKey<SpendAuth>,
pub validator_spend_key: SpendKeyBytes,
/// Consensus key for tendermint.
pub validator_cons_sk: tendermint::PrivateKey,
pub validator_cons_pk: tendermint::PublicKey,
/// P2P auth key for tendermint.
pub node_key_sk: tendermint::PrivateKey,
/// The identity key for the validator.
pub identity_key: IdentityKey,
#[allow(unused_variables, dead_code)]
pub node_key_pk: tendermint::PublicKey,
}

impl ValidatorKeys {
/// Use a hard-coded seed to generate a new set of validator keys.
pub fn from_seed(seed: [u8; 32]) -> Self {
// Create the spend key for this node.
let seed = SpendKeyBytes(seed);
let spend_key = SpendKey::from(seed.clone());

// Create signing key and verification key for this node.
let validator_id_sk = spend_key.spend_auth_key();
let validator_id_vk = VerificationKey::from(validator_id_sk);

let validator_cons_sk = ed25519_consensus::SigningKey::from(seed.0);

// generate consensus key for tendermint.
let validator_cons_sk = tendermint::PrivateKey::Ed25519(
validator_cons_sk
.as_bytes()
.as_slice()
.try_into()
.expect("32 bytes"),
);
let validator_cons_pk = validator_cons_sk.public_key();

// generate P2P auth key for tendermint.
let node_key_sk = ed25519_consensus::SigningKey::from(seed.0);
let signing_key_bytes = node_key_sk.as_bytes().as_slice();

// generate consensus key for tendermint.
let node_key_sk =
tendermint::PrivateKey::Ed25519(signing_key_bytes.try_into().expect("32 bytes"));
let node_key_pk = node_key_sk.public_key();

let identity_key: IdentityKey = IdentityKey(
spend_key
.full_viewing_key()
.spend_verification_key()
.clone()
.into(),
);
ValidatorKeys {
validator_id_sk: validator_id_sk.clone(),
validator_id_vk,
validator_cons_sk,
validator_cons_pk,
node_key_sk,
node_key_pk,
validator_spend_key: seed,
identity_key,
}
}
}

/// A genesis state that can be fed into CometBFT as well,
/// for verifying compliance of the mock tendermint implementation.
pub fn get_verified_genesis() -> Result<Genesis> {
let start_time = tendermint::Time::parse_from_rfc3339("2022-02-11T17:30:50.425417198Z")?;
let vkeys_a = ValidatorKeys::from_seed([0u8; 32]);

// TODO: make it possible to flag exporting the app state, keys, etc.
// to files possible on the builder
// genesis contents need to contain validator information in the app state
let mut genesis_contents =
genesis::Content::default().with_chain_id(TestNode::<()>::CHAIN_ID.to_string());

let spend_key_a = SpendKey::from(vkeys_a.validator_spend_key.clone());
let validator_a = Validator {
identity_key: Some(IdentityKey(vkeys_a.validator_id_vk.into()).into()),
governance_key: Some(GovernanceKey(spend_key_a.spend_auth_key().into()).into()),
consensus_key: vkeys_a.validator_cons_pk.to_bytes(),
name: "test".to_string(),
website: "https://example.com".to_string(),
description: "test".to_string(),
enabled: true,
funding_streams: vec![],
sequence_number: 0,
};

// let's only do one validator per chain for now
// since it's easier to validate against cometbft
genesis_contents
.stake_content
.validators
.push(validator_a.clone());

// the validator needs some initial delegations
let identity_key_a: IdentityKey = IdentityKey(
spend_key_a
.full_viewing_key()
.spend_verification_key()
.clone()
.into(),
);
let delegation_id_a = DelegationToken::from(&identity_key_a).denom();
let ivk_a = spend_key_a.incoming_viewing_key();
genesis_contents
.shielded_pool_content
.allocations
.push(Allocation {
address: ivk_a.payment_address(0u32.into()).0,
raw_amount: (25_000 * 10u128.pow(6)).into(),
raw_denom: delegation_id_a.to_string(),
});

let genesis = Genesis {
genesis_time: start_time.clone(),
chain_id: genesis_contents
.chain_id
.parse::<tendermint::chain::Id>()
.context("failed to parse chain ID")?,
initial_height: 0,
consensus_params: tendermint::consensus::Params {
abci: AbciParams::default(),
block: tendermint::block::Size {
// 1MB
max_bytes: MAX_BLOCK_TXS_PAYLOAD_BYTES as u64,
// Set to infinity since a chain running Penumbra won't use
// cometbft's notion of gas.
max_gas: -1,
// Minimum time increment between consecutive blocks.
time_iota_ms: 500,
},
evidence: tendermint::evidence::Params {
// We should keep this in approximate sync with the recommended default for
// `StakeParameters::unbonding_delay`, this is roughly a week.
max_age_num_blocks: 130000,
// Similarly, we set the max age duration for evidence to be a little over a week.
max_age_duration: tendermint::evidence::Duration(Duration::from_secs(650000)),
// 30KB
max_bytes: MAX_EVIDENCE_SIZE_BYTES as i64,
},
validator: tendermint::consensus::params::ValidatorParams {
pub_key_types: vec![Algorithm::Ed25519],
},
version: Some(tendermint::consensus::params::VersionParams { app: 0 }),
},
// always empty in genesis json
app_hash: tendermint::AppHash::default(),
// app_state: genesis_contents.into(),
app_state: serde_json::value::to_value(penumbra_app::genesis::AppState::Content(
genesis_contents,
))
.unwrap(),
// Set empty validator set for Tendermint config, which falls back to reading
// validators from the AppState, via ResponseInitChain:
// https://docs.tendermint.com/v0.32/tendermint-core/using-tendermint.html
validators: vec![],
};

Ok(genesis)
}
Loading

0 comments on commit 8ee733c

Please sign in to comment.