-
Notifications
You must be signed in to change notification settings - Fork 76
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
refactor(proto)!: update SignedTransaction
to contain Any
for transaction
#1044
Conversation
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.
I think we could amend the SignedTransaction
type to contain the raw bytes that we signed over, i.e. do something like this:
struct SignedTransaction {
signature: Signature,
verification_key: VerificationKey,
transaction: UnsignedTransaction,
transaction_bytes: Bytes,
}
Because we don't allow mutation of the signed transaction we can ensure that the signature always stays valid.
pub fn into_any(self) -> pbjson_types::Any { | ||
let raw = self.into_raw(); | ||
pbjson_types::Any { | ||
type_url: UNSIGNED_TRANSACTION_TYPE_URL.to_string(), |
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.
Please prost::Name::type_url
for this.
@@ -234,6 +246,15 @@ impl UnsignedTransaction { | |||
} | |||
} | |||
|
|||
#[must_use] | |||
pub fn to_any(&self) -> pbjson_types::Any { | |||
let raw = self.to_raw(); |
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.
You can impl this method in terms of into_any
(there is no perf advantage right now I think in specializing it and it avoids dulicating code):
pub fn to_any(&self) -> pbjson_types::Any {
self.clone().into_any()
}
/// - if the type URL is not the expected type URL | ||
/// - if the bytes in the [`Any`] do not decode to an [`UnsignedTransaction`] | ||
pub fn try_from_any(any: pbjson_types::Any) -> Result<Self, UnsignedTransactionError> { | ||
if any.type_url != UNSIGNED_TRANSACTION_TYPE_URL { |
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.
I believe we are deriving prost::Name
for all types:
if any.type_url != UnsignedTransactionError::type_url() {
#[error("invalid type URL")] | ||
InvalidTypeUrl, |
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.
Useful to provide a bit more info:
#[error("invalid type URL")] | |
InvalidTypeUrl, | |
#[error("encountered invalid type URL when converting from `google.protobuf.Any`; got `{got}`, expected `{}`", UnsignedTransaction::type_url())] | |
InvalidTypeUrl { got: String, }, |
@@ -282,6 +327,10 @@ enum UnsignedTransactionErrorKind { | |||
Action(#[source] action::ActionError), | |||
#[error("`params` field is unset")] | |||
UnsetParams(), | |||
#[error("invalid type URL")] | |||
InvalidTypeUrl, | |||
#[error("invalid bytes in Any")] |
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.
I would be a tad more specific about this. It might be that
#[error("invalid bytes in Any")] | |
#[error("failed to decode `google.protobuf.Any` to `{}`", UnsignedTransaction::type_url())] |
#[error("invalid type URL")] | ||
InvalidTypeUrl, | ||
#[error("invalid bytes in Any")] | ||
InvalidAnyBytes(#[source] prost::DecodeError), |
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.
InvalidAnyBytes(#[source] prost::DecodeError), | |
DecodeAny(#[source] prost::DecodeError), |
@@ -10,6 +10,9 @@ use super::raw; | |||
pub mod action; | |||
pub use action::Action; | |||
|
|||
pub const UNSIGNED_TRANSACTION_TYPE_URL: &str = | |||
"/astria.protocol.transaction.v1alpha1.UnsignedTransaction"; |
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.
I am not generally opposed to doing this (to save some allocations), but we must ensure that const matches the generated type URL of the prost::Name
trait. So either use UnsignedTransaction::type_url()
everywhere, or alternatively use the hardcoded value here, but add a unit test like below.
#[test]
fn hardcoded_type_url_matches_generated_trait() {
assert_eq!(UNSIGNED_TRANSACTION_TYPE_URL, &UnsignedTransaction::type_url(),);
}
Read my other comments re the use of Name::type_url()
with this in mind - either use the trait method everywhere, or ensure that the const always stays valid.
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.
updated to use type_url()
@SuperFluffy addressed comments! what's the point of keeping the tx bytes inside the |
@@ -95,7 +98,7 @@ impl SignedTransaction { | |||
raw::SignedTransaction { | |||
signature: signature.to_bytes().to_vec(), | |||
public_key: verification_key.to_bytes().to_vec(), | |||
transaction: Some(transaction.into_raw()), |
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.
What is the use case for calling into_raw
or to_raw
on a SignedTransaction
? Since we don't persist the serialized bytes in try_from_raw
we lose the determinism of the what is returned here. The signature may not match the transaction bytes on a roundtrip. If you deserialized from raw, reserializing into is not safe.
My thinking is we can either remove this if there isn't a use for them, or we should maintain a copy of the bytes of the transaction when creating SignedTransaction
and then build and return the any object here. I know not as pretty, and in some usecases it would be fine (ie I just constructed a new transaction and signed it), but the loops wouldn't be safe and avoiding them ever happening would require we add some data saying like "derived from raw" and then not allow reserializing (assuming we aren't perfect at code review).
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.
yeah, i updated this to keep the transaction_bytes
in SignedTransaction
so converting to raw is now guaranteed to be the same
My thinking is in similar vain to what @joroshiba had in their comment: it's not about the signature but about maintaining the exact same byte-representation when going
Right now, if you do step 2, i.e. If you keep the original bytes around you can completely avoid the issue. |
@SuperFluffy that makes sense, added to |
Summary
as title says
Background
protobuf encoding is not deterministic, so we want to verify the signature over the exact same bytes signed.
Changes
SignedTransaction
to containAny
for transaction instead ofUnsignedTransaction
Testing
unit test
Breaking Changelist
SignedTransaction
type is changedRelated Issues
closes #310