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

init validator tx - verify ownership of used keys #2163

Merged
merged 7 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Require to verify ownership of all validator keys when initialized on-chain.
([\#2163](https://github.com/anoma/namada/pull/2163))
35 changes: 32 additions & 3 deletions apps/src/lib/client/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use rand::rngs::OsRng;
use super::rpc;
use crate::cli::{args, safe_exit};
use crate::client::rpc::query_wasm_code_hash;
use crate::client::tx::signing::{default_sign, SigningTxData};
use crate::client::tx::signing::{
default_sign, init_validator_signing_data, SigningTxData,
};
use crate::client::tx::tx::ProcessTxResponse;
use crate::config::TendermintMode;
use crate::facade::tendermint_rpc::endpoint::broadcast::tx_sync::Response;
Expand Down Expand Up @@ -363,6 +365,7 @@ pub async fn submit_init_validator<'a>(

let validator_key_alias = format!("{}-key", alias);
let consensus_key_alias = format!("{}-consensus-key", alias);
let protocol_key_alias = format!("{}-protocol-key", alias);

let threshold = match threshold {
Some(threshold) => threshold,
Expand Down Expand Up @@ -477,7 +480,25 @@ pub async fn submit_init_validator<'a>(
scheme,
)
.unwrap();
let protocol_key = validator_keys.get_protocol_keypair().ref_to();
let protocol_sk = validator_keys.get_protocol_keypair();
let protocol_key = protocol_sk.to_public();

// Store the protocol key in the wallet so that we can sign the tx with it
// to verify ownership
display_line!(namada.io(), "Storing protocol key in the wallet...");
let password = read_and_confirm_encryption_password(unsafe_dont_encrypt);
namada
.wallet_mut()
.await
.insert_keypair(
protocol_key_alias,
tx_args.wallet_alias_force,
protocol_sk.clone(),
password,
None,
None,
)
.map_err(|err| error::Error::Other(err.to_string()))?;

let validator_vp_code_hash =
query_wasm_code_hash(namada, validator_vp_code_path.to_str().unwrap())
Expand Down Expand Up @@ -546,9 +567,17 @@ pub async fn submit_init_validator<'a>(
validator_vp_code_hash: extra_section_hash,
};

// Put together all the PKs that we have to sign with to verify ownership
let mut all_pks = data.account_keys.clone();
all_pks.push(consensus_key.to_public());
all_pks.push(eth_cold_pk);
all_pks.push(eth_hot_pk);
all_pks.push(data.protocol_key.clone());

tx.add_code_from_hash(tx_code_hash).add_data(data);

let signing_data = aux_signing_data(namada, &tx_args, None, None).await?;
let signing_data =
init_validator_signing_data(namada, &tx_args, all_pks).await?;

tx::prepare_tx(
namada,
Expand Down
45 changes: 45 additions & 0 deletions sdk/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,51 @@ pub async fn aux_signing_data<'a>(
})
}

pub async fn init_validator_signing_data<'a>(
context: &impl Namada<'a>,
args: &args::Tx<SdkTypes>,
validator_keys: Vec<common::PublicKey>,
) -> Result<SigningTxData, Error> {
let mut public_keys = if args.wrapper_fee_payer.is_none() {
tx_signers(context, args, None).await?
} else {
vec![]
};
public_keys.extend(validator_keys.clone());

let account_public_keys_map =
Some(AccountPublicKeysMap::from_iter(validator_keys));

let fee_payer = if args.disposable_signing_key {
context
.wallet_mut()
.await
.gen_disposable_signing_key(&mut OsRng)
.to_public()
} else {
match &args.wrapper_fee_payer {
Some(keypair) => keypair.to_public(),
None => public_keys.get(0).ok_or(TxError::InvalidFeePayer)?.clone(),
}
};

if fee_payer == masp_tx_key().to_public() {
other_err(
"The gas payer cannot be the MASP, please provide a different gas \
payer."
.to_string(),
)?;
}

Ok(SigningTxData {
owner: None,
public_keys,
threshold: 0,
Copy link
Collaborator

Choose a reason for hiding this comment

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

what does a threshold of 0 mean here?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's unused for signing - this field is only used for offline proposal votes

account_public_keys_map,
fee_payer,
})
}

/// Informations about the post-tx balance of the tx's source. Used to correctly
/// handle fee validation in the wrapper tx
pub struct TxSourcePostBalance {
Expand Down
79 changes: 79 additions & 0 deletions shared/src/vm/host_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,85 @@ where
sentinel.set_invalid_commitment();
}

/// Verify a transaction signature
#[allow(clippy::too_many_arguments)]
pub fn tx_verify_tx_section_signature<MEM, DB, H, CA>(
env: &TxVmEnv<MEM, DB, H, CA>,
hash_list_ptr: u64,
hash_list_len: u64,
public_keys_map_ptr: u64,
public_keys_map_len: u64,
threshold: u8,
max_signatures_ptr: u64,
max_signatures_len: u64,
) -> TxResult<i64>
where
MEM: VmMemory,
DB: storage::DB + for<'iter> storage::DBIter<'iter>,
H: StorageHasher,
CA: WasmCacheAccess,
{
let (hash_list, gas) = env
.memory
.read_bytes(hash_list_ptr, hash_list_len as _)
.map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?;

let sentinel = unsafe { env.ctx.sentinel.get() };
tx_charge_gas(env, gas)?;
let hashes = <[Hash; 1]>::try_from_slice(&hash_list)
.map_err(TxRuntimeError::EncodingError)?;

let (public_keys_map, gas) = env
.memory
.read_bytes(public_keys_map_ptr, public_keys_map_len as _)
.map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?;
tx_charge_gas(env, gas)?;
let public_keys_map =
namada_core::types::account::AccountPublicKeysMap::try_from_slice(
&public_keys_map,
)
.map_err(TxRuntimeError::EncodingError)?;

tx_charge_gas(env, gas)?;

let (max_signatures, gas) = env
.memory
.read_bytes(max_signatures_ptr, max_signatures_len as _)
.map_err(|e| TxRuntimeError::MemoryError(Box::new(e)))?;
tx_charge_gas(env, gas)?;
let max_signatures = Option::<u8>::try_from_slice(&max_signatures)
.map_err(TxRuntimeError::EncodingError)?;

let tx = unsafe { env.ctx.tx.get() };

tx_charge_gas(
env,
gas::VERIFY_TX_SIG_GAS * public_keys_map.idx_to_pk.len() as u64,
)?;
match tx.verify_signatures(
&hashes,
public_keys_map,
&None,
threshold,
max_signatures,
// This uses VpGasMeter, so we're charging the gas here manually
// instead
&mut None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor thing here, we can change the verify_signatures method of Tx to accept an &mut dyn GasMetering trait object just like we do in fetch_or_compile so that we can still charge gas for every single signature without the need to do it manually, if you prefer. Alternatively, since this function is only called by the init_validator tx, and we always require 4 valid signatures, we could also just skip charging gas in here since its cost is already included in the benchmark of the tx itself

Copy link
Member Author

Choose a reason for hiding this comment

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

I've refactored it in 88578bf but without the dynamic dispatch using a lambda instead. It also moves the gas metering logic back into the relevant places in vm/host_env.rs instead of being done in proto/types.rs

) {
Ok(_) => Ok(HostEnvResult::Success.to_i64()),
Err(err) => match err {
namada_core::proto::Error::OutOfGas(inner) => {
sentinel.set_out_of_gas();
Err(TxRuntimeError::OutOfGas(inner))
}
namada_core::proto::Error::InvalidSectionSignature(_) => {
Ok(HostEnvResult::Fail.to_i64())
}
_ => Ok(HostEnvResult::Fail.to_i64()),
},
}
}

/// Evaluate a validity predicate with the given input data.
pub fn vp_eval<MEM, DB, H, EVAL, CA>(
env: &VpVmEnv<'static, MEM, DB, H, EVAL, CA>,
Expand Down
3 changes: 2 additions & 1 deletion shared/src/vm/wasm/host_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ where
"namada_tx_get_native_token" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_get_native_token),
"namada_tx_log_string" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_log_string),
"namada_tx_ibc_execute" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_ibc_execute),
"namada_tx_set_commitment_sentinel" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_set_commitment_sentinel)
"namada_tx_set_commitment_sentinel" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_set_commitment_sentinel),
"namada_tx_verify_tx_section_signature" => Function::new_native_with_env(wasm_store, env.clone(), host_env::tx_verify_tx_section_signature),
},
}
}
Expand Down
9 changes: 9 additions & 0 deletions tests/src/vm_host_env/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,13 @@ mod native_tx_host_env {
native_host_fn!(tx_log_string(str_ptr: u64, str_len: u64));
native_host_fn!(tx_charge_gas(used_gas: u64));
native_host_fn!("non-result", tx_set_commitment_sentinel());
native_host_fn!(tx_verify_tx_section_signature(
hash_list_ptr: u64,
hash_list_len: u64,
public_keys_map_ptr: u64,
public_keys_map_len: u64,
threshold: u8,
max_signatures_ptr: u64,
max_signatures_len: u64,
) -> i64);
}
37 changes: 36 additions & 1 deletion tx_prelude/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use std::marker::PhantomData;
pub use borsh::{BorshDeserialize, BorshSerialize};
pub use borsh_ext;
use borsh_ext::BorshSerializeExt;
pub use namada_core::ledger::eth_bridge;
pub use namada_core::ledger::governance::storage as gov_storage;
pub use namada_core::ledger::parameters::storage as parameters_storage;
pub use namada_core::ledger::storage::types::encode;
Expand All @@ -28,11 +27,14 @@ pub use namada_core::ledger::storage_api::{
ResultExt, StorageRead, StorageWrite,
};
pub use namada_core::ledger::tx_env::TxEnv;
pub use namada_core::ledger::{eth_bridge, parameters};
pub use namada_core::proto::{Section, Tx};
use namada_core::types::account::AccountPublicKeysMap;
pub use namada_core::types::address::Address;
use namada_core::types::chain::CHAIN_ID_LENGTH;
pub use namada_core::types::ethereum_events::EthAddress;
use namada_core::types::internal::HostEnvResult;
use namada_core::types::key::common;
use namada_core::types::storage::TxIndex;
pub use namada_core::types::storage::{
self, BlockHash, BlockHeight, Epoch, Header, BLOCK_HASH_LENGTH,
Expand Down Expand Up @@ -361,3 +363,36 @@ impl TxEnv for Ctx {
pub fn tx_ibc_execute() {
unsafe { namada_tx_ibc_execute() }
}

/// Verify section signatures against the given list of keys
pub fn verify_signatures_of_pks(
ctx: &Ctx,
tx: &Tx,
pks: Vec<common::PublicKey>,
) -> EnvResult<bool> {
let max_signatures_per_transaction =
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if the max_signatures_per_transaction is lower than the number of validator keys at initialization?

Copy link
Member Author

Choose a reason for hiding this comment

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

As we don't have support for validator multisig, it would only be an issue if the source of the tx is a mutisig with threshold > max_signatures_per_transaction - 5. We use 15 as the default which should be sufficient for either case

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, I was wrong on this - we do have multisig validator support, but with the default this would still allow up to 10 multisig participants which should be sufficient

parameters::max_signatures_per_transaction(ctx)?;

// Require signatures from all the given keys
let threshold = u8::try_from(pks.len()).into_storage_result()?;
let public_keys_index_map = AccountPublicKeysMap::from_iter(pks);

// Serialize parameters
let max_signatures = max_signatures_per_transaction.serialize_to_vec();
let public_keys_map = public_keys_index_map.serialize_to_vec();
let targets = [tx.raw_header_hash()].serialize_to_vec();

let valid = unsafe {
namada_tx_verify_tx_section_signature(
targets.as_ptr() as _,
targets.len() as _,
public_keys_map.as_ptr() as _,
public_keys_map.len() as _,
threshold,
max_signatures.as_ptr() as _,
max_signatures.len() as _,
)
};

Ok(HostEnvResult::is_success(valid))
}
12 changes: 12 additions & 0 deletions vm_env/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ pub mod tx {

/// Set the sentinel for a wrong tx section commitment
pub fn namada_tx_set_commitment_sentinel();

// Verify the signatures of a tx
pub fn namada_tx_verify_tx_section_signature(
hash_list_ptr: u64,
hash_list_len: u64,
public_keys_map_ptr: u64,
public_keys_map_len: u64,
threshold: u8,
max_signatures_ptr: u64,
max_signatures_len: u64,
) -> i64;

}
}

Expand Down
16 changes: 16 additions & 0 deletions wasm/wasm_source/src/tx_init_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult {
.code
.hash();

// Check that the tx has been signed with all the keys to be used for the
// validator account
let mut all_pks = init_validator.account_keys.clone();
all_pks.push(init_validator.consensus_key.clone());
all_pks.push(key::common::PublicKey::Secp256k1(
init_validator.eth_cold_key.clone(),
));
all_pks.push(key::common::PublicKey::Secp256k1(
init_validator.eth_hot_key.clone(),
));
all_pks.push(init_validator.protocol_key.clone());
if !matches!(verify_signatures_of_pks(ctx, &signed, all_pks), Ok(true)) {
debug_log!("Keys ownership signature verification failed");
panic!()
}

// Register the validator in PoS
match ctx.init_validator(init_validator, validator_vp_code_hash) {
Ok(validator_address) => {
Expand Down
Loading