-
Notifications
You must be signed in to change notification settings - Fork 956
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
Outsource masp signature verification #3372
Changes from all commits
c6f11bc
17b9fcf
3369502
5f70e2c
ffd5cfd
01484b3
77b164d
14d70f3
dab10a6
fc63450
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
- Moved the signature verifications out of the masp vp and into the affected | ||
addresses' vps. ([\#3312](https://github.com/anoma/namada/issues/3312)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
- Eliminates the MASP VPs requirement for all debited accounts to sign a Tx. | ||
([\#3516](https://github.com/anoma/namada/pull/3516)) |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -21,7 +21,6 @@ use namada_core::ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcM | |||
use namada_core::ibc::apps::transfer::types::packet::PacketData; | ||||
use namada_core::masp::{addr_taddr, encode_asset_type, ibc_taddr, MaspEpoch}; | ||||
use namada_core::storage::Key; | ||||
use namada_gas::GasMetering; | ||||
use namada_governance::storage::is_proposal_accepted; | ||||
use namada_ibc::core::channel::types::commitment::{ | ||||
compute_packet_commitment, PacketCommitment, | ||||
|
@@ -43,6 +42,7 @@ use namada_sdk::masp::TAddrData; | |||
use namada_state::{ConversionState, OptionExt, ResultExt, StateRead}; | ||||
use namada_token::read_denom; | ||||
use namada_token::validation::verify_shielded_tx; | ||||
use namada_tx::action::Read; | ||||
use namada_tx::BatchedTxRef; | ||||
use namada_vp_env::VpEnv; | ||||
use thiserror::Error; | ||||
|
@@ -687,33 +687,26 @@ where | |||
.read_post(&counterpart_balance_key)? | ||||
.unwrap_or_default(); | ||||
// Public keys must be the hash of the sources/targets | ||||
let address_hash = addr_taddr(counterpart.clone()); | ||||
let addr_hash = addr_taddr(counterpart.clone()); | ||||
// Enable the decoding of these counterpart addresses | ||||
result | ||||
.decoder | ||||
.insert(address_hash, TAddrData::Addr(counterpart.clone())); | ||||
.insert(addr_hash, TAddrData::Addr(counterpart.clone())); | ||||
let zero = ValueSum::zero(); | ||||
// Finally record the actual balance change starting with the initial | ||||
// state | ||||
let pre_entry = result | ||||
.pre | ||||
.get(&address_hash) | ||||
.cloned() | ||||
.unwrap_or(ValueSum::zero()); | ||||
let pre_entry = result.pre.get(&addr_hash).unwrap_or(&zero).clone(); | ||||
result.pre.insert( | ||||
address_hash, | ||||
addr_hash, | ||||
checked!( | ||||
pre_entry + &ValueSum::from_pair((*token).clone(), pre_balance) | ||||
) | ||||
.map_err(native_vp::Error::new)?, | ||||
); | ||||
// And then record thee final state | ||||
let post_entry = result | ||||
.post | ||||
.get(&address_hash) | ||||
.cloned() | ||||
.unwrap_or(ValueSum::zero()); | ||||
// And then record the final state | ||||
let post_entry = result.post.get(&addr_hash).cloned().unwrap_or(zero); | ||||
result.post.insert( | ||||
address_hash, | ||||
addr_hash, | ||||
checked!( | ||||
post_entry | ||||
+ &ValueSum::from_pair((*token).clone(), post_balance) | ||||
|
@@ -757,6 +750,7 @@ where | |||
&self, | ||||
tx_data: &BatchedTxRef<'_>, | ||||
keys_changed: &BTreeSet<Key>, | ||||
verifiers: &BTreeSet<Address>, | ||||
) -> Result<()> { | ||||
let masp_epoch_multiplier = | ||||
namada_parameters::read_masp_epoch_multiplier_parameter( | ||||
|
@@ -771,6 +765,7 @@ where | |||
})?; | ||||
let conversion_state = self.ctx.state.in_mem().get_conversion_state(); | ||||
let ibc_msg = self.ctx.get_ibc_message(tx_data).ok(); | ||||
let actions = self.ctx.read_actions()?; | ||||
let shielded_tx = | ||||
if let Some(IbcMessage::Envelope(ref envelope)) = ibc_msg { | ||||
extract_masp_tx_from_envelope(envelope).ok_or_else(|| { | ||||
|
@@ -781,7 +776,7 @@ where | |||
} else { | ||||
// Get the Transaction object from the actions | ||||
let masp_section_ref = | ||||
namada_tx::action::get_masp_section_ref(&self.ctx)? | ||||
namada_tx::action::get_masp_section_ref(&actions) | ||||
.ok_or_else(|| { | ||||
native_vp::Error::new_const( | ||||
"Missing MASP section reference in action", | ||||
|
@@ -809,27 +804,29 @@ where | |||
} | ||||
|
||||
// Check the validity of the keys and get the transfer data | ||||
let mut changed_balances = | ||||
let changed_balances = | ||||
self.validate_state_and_get_transfer_data(keys_changed, ibc_msg)?; | ||||
|
||||
// Some constants that will be used repeatedly | ||||
let zero = ValueSum::zero(); | ||||
let masp_address_hash = addr_taddr(MASP); | ||||
verify_sapling_balancing_value( | ||||
changed_balances | ||||
.pre | ||||
.get(&masp_address_hash) | ||||
.unwrap_or(&ValueSum::zero()), | ||||
.unwrap_or(&zero), | ||||
changed_balances | ||||
.post | ||||
.get(&masp_address_hash) | ||||
.unwrap_or(&ValueSum::zero()), | ||||
.unwrap_or(&zero), | ||||
&shielded_tx.sapling_value_balance(), | ||||
masp_epoch, | ||||
&changed_balances.tokens, | ||||
conversion_state, | ||||
)?; | ||||
|
||||
// The set of addresses that are required to authorize this transaction | ||||
let mut signers = BTreeSet::new(); | ||||
let mut authorizers = BTreeSet::new(); | ||||
|
||||
// Checks on the sapling bundle | ||||
// 1. The spend descriptions' anchors are valid | ||||
|
@@ -845,32 +842,56 @@ where | |||
self.valid_note_commitment_update(&shielded_tx)?; | ||||
|
||||
// Checks on the transparent bundle, if present | ||||
let mut changed_bals_minus_txn = changed_balances.clone(); | ||||
validate_transparent_bundle( | ||||
&shielded_tx, | ||||
&mut changed_balances, | ||||
&mut changed_bals_minus_txn, | ||||
masp_epoch, | ||||
conversion_state, | ||||
&mut signers, | ||||
&mut authorizers, | ||||
)?; | ||||
|
||||
// Ensure that every account for which balance has gone down has | ||||
// authorized this transaction | ||||
for (addr, pre) in changed_balances.pre { | ||||
if changed_balances | ||||
.post | ||||
.get(&addr) | ||||
.unwrap_or(&ValueSum::zero()) | ||||
< &pre | ||||
&& addr != masp_address_hash | ||||
// Ensure that every account for which balance has gone down as a result | ||||
// of the Transaction has authorized this transaction | ||||
for (addr, minus_txn_pre) in changed_bals_minus_txn.pre { | ||||
// The pre-balance seen by all VPs including this one | ||||
let pre = changed_balances.pre.get(&addr).unwrap_or(&zero); | ||||
// The post-balance seen by all VPs including this one | ||||
let post = changed_balances.post.get(&addr).unwrap_or(&zero); | ||||
// The post-balance if the effects of the Transaction are removed | ||||
let minus_txn_post = | ||||
changed_bals_minus_txn.post.get(&addr).unwrap_or(&zero); | ||||
// Never require a signature from the MASP VP | ||||
if addr != masp_address_hash && | ||||
// Only require further authorization if without the Transaction, | ||||
// this Tx would decrease the balance of this address | ||||
minus_txn_post < &minus_txn_pre && | ||||
// Only require further authorization from this address if the | ||||
// Transaction alters its balance | ||||
(minus_txn_pre, minus_txn_post) != (pre.clone(), post) | ||||
{ | ||||
signers.insert(addr); | ||||
// This address will need to provide further authorization | ||||
authorizers.insert(addr); | ||||
} | ||||
} | ||||
|
||||
let mut actions_authorizers: HashSet<&Address> = actions | ||||
.iter() | ||||
.filter_map(|action| { | ||||
if let namada_tx::action::Action::Masp( | ||||
namada_tx::action::MaspAction::MaspAuthorizer(addr), | ||||
) = action | ||||
{ | ||||
Some(addr) | ||||
} else { | ||||
None | ||||
} | ||||
}) | ||||
.collect(); | ||||
// Ensure that this transaction is authorized by all involved parties | ||||
for signer in signers { | ||||
for authorizer in authorizers { | ||||
if let Some(TAddrData::Addr(IBC)) = | ||||
changed_balances.decoder.get(&signer) | ||||
changed_bals_minus_txn.decoder.get(&authorizer) | ||||
{ | ||||
// If the IBC address is a signatory, then it means that either | ||||
// Tx - Transaction(s) causes a decrease in the IBC balance or | ||||
|
@@ -887,7 +908,7 @@ where | |||
if let Some(transp_bundle) = shielded_tx.transparent_bundle() { | ||||
for vout in transp_bundle.vout.iter() { | ||||
if let Some(TAddrData::Ibc(_)) = | ||||
changed_balances.decoder.get(&vout.address) | ||||
changed_bals_minus_txn.decoder.get(&vout.address) | ||||
{ | ||||
let error = native_vp::Error::new_const( | ||||
"Simultaneous credit and debit of IBC account \ | ||||
|
@@ -900,39 +921,53 @@ where | |||
} | ||||
} | ||||
} else if let Some(TAddrData::Addr(signer)) = | ||||
changed_balances.decoder.get(&signer) | ||||
changed_bals_minus_txn.decoder.get(&authorizer) | ||||
{ | ||||
// Otherwise the signer must be decodable so that we can | ||||
// manually check the signatures | ||||
let public_keys_index_map = | ||||
crate::account::public_keys_index_map( | ||||
&self.ctx.pre(), | ||||
signer, | ||||
)?; | ||||
let threshold = | ||||
crate::account::threshold(&self.ctx.pre(), signer)? | ||||
.unwrap_or(1); | ||||
let mut gas_meter = self.ctx.gas_meter.borrow_mut(); | ||||
tx_data | ||||
.tx | ||||
.verify_signatures( | ||||
&[tx_data.tx.raw_header_hash()], | ||||
public_keys_index_map, | ||||
&Some(signer.clone()), | ||||
threshold, | ||||
|| gas_meter.consume(crate::gas::VERIFY_TX_SIG_GAS), | ||||
) | ||||
.map_err(native_vp::Error::new)?; | ||||
// Otherwise the owner's vp must have been triggered and the | ||||
// relative action must have been written | ||||
if !verifiers.contains(signer) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would almost certainly agree with this logic if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this PR only removes the signature verification and requests the corresponding VP to be triggered. As you are saying though, Bertha's VP would not reject the tx in the attack scenario you described because the balance change is 0. To make this work I've introduced a new masp For this logic to work we need some cooperation from the tx which must write the temporary action key(s) required (the ones carrying the addresses whose extra signatures have been attached to the tx). I believe this logic is ok (but please double check again) but I think that there are other flaws in this this implementation of mine:
Note that, as I've already answered Tomas before, at the moment the transactions do not support this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a single transfer I think we would need do something like this:
For these to work together the MASP VP should then ensure that when the action is However, with the scenario with multiple transfers @murisi described I think this may not be sufficient. A tx could only attach a single action despite performing multiple actions. We might then need to add more information to the actions to describe the transfers in more detail and the MASP and user/implicit VPs will have to enforce that the storage changes match these actions. In this example, for the unshielding action from Alice we could have e.g.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I completely misread the approach taken by this PR. My bad.
I see. And at the bare minimum, custom VPs must not forget to handle MASP
Nice, I see you've now covered this case.
Nice, I see you've also covered this case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
At least in the current PR, this is all covered right? The current conditional forces the presence of the signer in the verifiers list. The next conditional forces the presence of the corresponding
The |
||||
let error = native_vp::Error::new_alloc(format!( | ||||
"The required vp of address {signer} was not triggered" | ||||
)) | ||||
.into(); | ||||
tracing::debug!("{error}"); | ||||
return Err(error); | ||||
} | ||||
|
||||
// The action is required becuse the target vp might have been | ||||
// triggered for other reasons but we need to signal it that it | ||||
// is required to validate a discrepancy in its balance change | ||||
// because of a masp transaction, which might require a | ||||
// different validation than a normal balance change | ||||
if !actions_authorizers.swap_remove(signer) { | ||||
let error = native_vp::Error::new_alloc(format!( | ||||
"The required masp authorizer action for address \ | ||||
{signer} is missing" | ||||
)) | ||||
.into(); | ||||
tracing::debug!("{error}"); | ||||
return Err(error); | ||||
} | ||||
} else { | ||||
// We are not able to decode the signer, so just fail | ||||
// We are not able to decode the authorizer, so just fail | ||||
let error = native_vp::Error::new_const( | ||||
"Unable to decode a transaction signer", | ||||
"Unable to decode a transaction authorizer", | ||||
) | ||||
.into(); | ||||
tracing::debug!("{error}"); | ||||
return Err(error); | ||||
} | ||||
} | ||||
// The transaction shall not push masp authorizer actions that are not | ||||
// needed cause this might lead vps to run a wrong validation logic | ||||
if !actions_authorizers.is_empty() { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is fine, though it means that transactions need to precisely compute/maintain the required set of authorizers even when the logic at
Transaction transparent inputs and hence may be inconvenient to compute and push in transactions.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's correct, there's some burden on the transaction side to match the set of verifiers. But eventually the transaction has to either match the set of authorizers required by the vps exactly or match a superset of it (in case we wanted to relax this condition). The only thing I can think of to ease the computation on the transaction side would be to push actions for all the involved parties (which need to be computed anyway since, as you are saying, they might not be limited to the transparent inputs). This would lead to an increase in gas cost given by the extra (unneeded) signatures attached to the transaction and by the cost of their verification: at the end this might outweigh the added gas cost for the logic to be placed in the transfer transaction. Another issue is that collecting some of these signatures could be complicated and make the ux more unpleasant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'd assumed that redundant MASP authorizer actions only imply signature checks for redundant signatures. In a correctly implemented VP, is this not necessarily the case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For our VPs, that's correct, we'd just recheck the signature (which actually doesn't even happen because of the |
||||
let error = native_vp::Error::new_const( | ||||
"Found masp authorizer actions that are not required", | ||||
) | ||||
.into(); | ||||
tracing::debug!("{error}"); | ||||
return Err(error); | ||||
} | ||||
|
||||
// Verify the proofs | ||||
verify_shielded_tx(&shielded_tx, |gas| self.ctx.charge_gas(gas)) | ||||
|
@@ -957,18 +992,17 @@ fn unepoched_tokens( | |||
Ok(()) | ||||
} | ||||
|
||||
// Handle transparent input | ||||
fn validate_transparent_input<A: Authorization>( | ||||
vin: &TxIn<A>, | ||||
changed_balances: &mut ChangedBalances, | ||||
transparent_tx_pool: &mut I128Sum, | ||||
epoch: MaspEpoch, | ||||
conversion_state: &ConversionState, | ||||
signers: &mut BTreeSet<TransparentAddress>, | ||||
authorizers: &mut BTreeSet<TransparentAddress>, | ||||
) -> Result<()> { | ||||
// A decrease in the balance of an account needs to be | ||||
// authorized by the account of this transparent input | ||||
signers.insert(vin.address); | ||||
authorizers.insert(vin.address); | ||||
// Non-masp sources add to the transparent tx pool | ||||
*transparent_tx_pool = transparent_tx_pool | ||||
.checked_add( | ||||
|
@@ -1044,7 +1078,6 @@ fn validate_transparent_input<A: Authorization>( | |||
Ok(()) | ||||
} | ||||
|
||||
// Handle transparent output | ||||
fn validate_transparent_output( | ||||
out: &TxOut, | ||||
changed_balances: &mut ChangedBalances, | ||||
|
@@ -1116,7 +1149,7 @@ fn validate_transparent_bundle( | |||
changed_balances: &mut ChangedBalances, | ||||
epoch: MaspEpoch, | ||||
conversion_state: &ConversionState, | ||||
signers: &mut BTreeSet<TransparentAddress>, | ||||
authorizers: &mut BTreeSet<TransparentAddress>, | ||||
) -> Result<()> { | ||||
// The Sapling value balance adds to the transparent tx pool | ||||
let mut transparent_tx_pool = shielded_tx.sapling_value_balance(); | ||||
|
@@ -1129,7 +1162,7 @@ fn validate_transparent_bundle( | |||
&mut transparent_tx_pool, | ||||
epoch, | ||||
conversion_state, | ||||
signers, | ||||
authorizers, | ||||
)?; | ||||
} | ||||
|
||||
|
@@ -1253,7 +1286,7 @@ where | |||
&self, | ||||
tx_data: &BatchedTxRef<'_>, | ||||
keys_changed: &BTreeSet<Key>, | ||||
_verifiers: &BTreeSet<Address>, | ||||
verifiers: &BTreeSet<Address>, | ||||
) -> Result<()> { | ||||
let masp_keys_changed: Vec<&Key> = | ||||
keys_changed.iter().filter(|key| is_masp_key(key)).collect(); | ||||
|
@@ -1283,7 +1316,7 @@ where | |||
self.is_valid_parameter_change(tx_data) | ||||
} else if masp_transfer_changes { | ||||
// The MASP transfer keys can only be changed by a valid Transaction | ||||
self.is_valid_masp_transfer(tx_data, keys_changed) | ||||
self.is_valid_masp_transfer(tx_data, keys_changed, verifiers) | ||||
} else { | ||||
// Changing no MASP keys at all is also fine | ||||
Ok(()) | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actions
are actually a vector ofAction
, so it's possible to push the same action more than once. I believe from the perspective of the masp vp this is of no importance: we just want to check that the action for a specific authorizer has been written, it doesn't matter if it's duplicated