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

Avoid duplicated signatures when building transactions #4230

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Improved the transaction building process to avoid duplicated sections.
([\#4230](https://github.com/anoma/namada/pull/4230))
47 changes: 37 additions & 10 deletions crates/sdk/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,45 @@ pub struct SigningTxData {

impl PartialEq for SigningTxData {
fn eq(&self, other: &Self) -> bool {
if !(self.owner == other.owner
&& self.threshold == other.threshold
&& self.account_public_keys_map == other.account_public_keys_map
&& self.fee_payer == other.fee_payer)
// Deconstruct the two instances to ensure we don't forget any new field
let SigningTxData {
owner,
public_keys,
threshold,
account_public_keys_map,
fee_payer,
shielded_hash,
} = self;
let SigningTxData {
owner: other_owner,
public_keys: other_public_keys,
threshold: other_threshold,
account_public_keys_map: other_account_public_keys_map,
fee_payer: other_fee_payer,
shielded_hash: other_shielded_hash,
} = other;

if !(owner == other_owner
&& threshold == other_threshold
&& account_public_keys_map == other_account_public_keys_map
&& fee_payer == other_fee_payer
&& shielded_hash == other_shielded_hash)
{
return false;
}

// Check equivalence of the public keys ignoring the specific ordering
if self.public_keys.len() != other.public_keys.len() {
return false;
}
// and duplicates (the PartialEq implementation of IndexSet ignores the
// order)
let unique_public_keys = HashSet::<
&namada_account::common::CommonPublicKey,
>::from_iter(public_keys.iter());
let unique_other_public_keys =
HashSet::<&namada_account::common::CommonPublicKey>::from_iter(
other_public_keys.iter(),
);

self.public_keys
.iter()
.all(|pubkey| other.public_keys.contains(pubkey))
unique_public_keys == unique_other_public_keys
}
}

Expand Down Expand Up @@ -313,6 +336,10 @@ where
}
}

// Before signing the wrapper tx prune all the possible duplicated sections
// (including duplicated raw signatures)
tx.prune_duplicated_sections();

// Then try signing the wrapper header (fee payer). Check if there's a
// provided wrapper signature, otherwise sign with the software wallet or
// use the fallback
Expand Down
4 changes: 4 additions & 0 deletions crates/sdk/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ pub fn dump_tx<IO: Io>(io: &IO, args: &args::Tx, mut tx: Tx) -> Result<()> {
));
}

// Remove duplicated sections before dumping. This is useful in case the
// dumped tx needed to be signed offline
tx.prune_duplicated_sections();

match args.output_folder.clone() {
Some(path) => {
let tx_path = path.join(format!(
Expand Down
36 changes: 35 additions & 1 deletion crates/tx/src/section.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,6 @@ pub enum Signer {
BorshSchema,
Serialize,
Deserialize,
PartialEq,
)]
pub struct Authorization {
/// The hash of the section being signed
Expand All @@ -442,6 +441,41 @@ pub struct Authorization {
pub signatures: BTreeMap<u8, common::Signature>,
}

impl PartialEq for Authorization {
fn eq(&self, other: &Self) -> bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it matter that partially equivalent Authorizations might not have the same cryptographic hash? Or is this impossible? Could this sort of definition of partial equality confuse people in the future? (This definition seems to be calculated specifically to deal with our duplicate signatures issue (no?), but could be unintuitive in other cases/situations...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't believe PartialEq should guarantee a byte-level match between the two instances but rather a logical equivalence (as I said in the previous question, when safety is critical one should explicitly rely on the digest of the expected instance).

I do see your point on this implementation being potentially confusing in other situations: I could try to reintroduce the check on the signer with a more refined logic. My only concern is the case when a multisig signer is expressed as a set of keys in one instance and as an address in the other one: in this case there's no way I can resolve the equivalence without a query to storage (but there's a chance we always express multisig signers as a set of keys which would solve this issue).

An alternative could be to come up with a different function for this comparison and restore the previous implementation of PartialEq (net of ignoring the order of the set of keys which I think is a required update regardless).

// Deconstruct the two instances to ensure we don't forget any new field
let Authorization {
targets,
signer: _,
signatures,
} = self;
let Authorization {
targets: other_targets,
signer: _,
signatures: other_signatures,
} = other;

// Two authorizations are equal when they are computed over the same
Copy link
Collaborator

@murisi murisi Jan 13, 2025

Choose a reason for hiding this comment

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

This definition could work. But it has the implication that if Authorization::signer contains the variant Signer::PubKeys and someone tampers with one of the public keys in that list so that Authorization::verify_signature can no longer pass, this PartialEq implementation would still say that the tampered instance is still equal to the original. No? If so, is this desirable?

Also, given that this function defines a partial equivalence relation, I guess false should be returned in those cases where there is insufficient information to check/conclude equality of the signers? No?

Copy link
Collaborator Author

@grarco grarco Jan 14, 2025

Choose a reason for hiding this comment

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

That's correct. The representation of the signer being different in the duplicated authorizations I could see on mainnet was the drive to modify this trait implementation. My reasoning was that once we establish the equality of the signature's targets and of the signatures themselves we can safely consider the two Authorizations to be the same even if the signers are expressed differently. In case of the signers being straight different (which would cause one of the two instances to fail the signature verification check) I believe we are looking at something else than just equality: I guess one should check the integrity of the sections at the byte level using a digest or similar techniques (see also my answer to your next question since it's related to this one)

// target(s) and the signatures match, regardless of how the signer is
// expressed

// Check equivalence of the targets ignoring the specific ordering and
// duplicates (the PartialEq implementation of IndexSet ignores the
// order)
let unique_targets =
HashSet::<&namada_account::Hash>::from_iter(targets.iter());
let unique_other_targets =
HashSet::<&namada_account::Hash>::from_iter(other_targets.iter());

if unique_targets != unique_other_targets {
return false;
}

// Check equivalence of the signatures
signatures == other_signatures
}
}

impl Authorization {
/// Sign the given section hash with the given key and return a section
pub fn new(
Expand Down
11 changes: 11 additions & 0 deletions crates/tx/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,17 @@ impl Tx {
self.header.batch.insert(cmt)
}

/// Remove duplicated sections from the transaction
pub fn prune_duplicated_sections(&mut self) {
let sections = std::mem::take(&mut self.sections);
let mut unique_sections = HashMap::with_capacity(sections.len());
for section in sections {
unique_sections.insert(section.get_hash(), section);
}

self.sections = unique_sections.into_values().collect();
}

/// Get the transaction header
pub fn header(&self) -> Header {
self.header.clone()
Expand Down
Loading