From 37a5e6b35bac481339e11e52c173462e3a5cb140 Mon Sep 17 00:00:00 2001 From: Sergei Boiko <127754187+satoshiotomakan@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:36:02 +0700 Subject: [PATCH] [Cosmos]: Add ability to sign direct with `TxBody` bytes only (#4202) * Generate `AuthInfo` from `SigningInput` parameters if `SignDirect.auth_info_bytes` empty --- .../src/modules/compiler/tw_compiler.rs | 4 +- rust/tw_cosmos_sdk/src/modules/tx_builder.rs | 53 ++++++++++++------- rust/tw_cosmos_sdk/tests/sign.rs | 39 ++++++++++++++ src/proto/Cosmos.proto | 2 + 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/rust/tw_cosmos_sdk/src/modules/compiler/tw_compiler.rs b/rust/tw_cosmos_sdk/src/modules/compiler/tw_compiler.rs index 544933f3964..a8550fd7fbb 100644 --- a/rust/tw_cosmos_sdk/src/modules/compiler/tw_compiler.rs +++ b/rust/tw_cosmos_sdk/src/modules/compiler/tw_compiler.rs @@ -62,7 +62,7 @@ impl TWTransactionCompiler { input: Proto::SigningInput<'_>, ) -> SigningResult> { let tx_hasher = TxBuilder::::tx_hasher_from_proto(&input); - let preimage = match TxBuilder::::try_sign_direct_args(&input) { + let preimage = match TxBuilder::::try_sign_direct_args(coin, &input) { // If there was a `SignDirect` message in the signing input, generate the tx preimage directly. Ok(Some(sign_direct_args)) => { ProtobufPreimager::::preimage_hash_direct(&sign_direct_args, tx_hasher)? @@ -128,7 +128,7 @@ impl TWTransactionCompiler { let params = TxBuilder::::public_key_params_from_proto(&input); let public_key = Context::PublicKey::from_bytes(coin, &public_key, params)?; - let signed_tx_raw = match TxBuilder::::try_sign_direct_args(&input) { + let signed_tx_raw = match TxBuilder::::try_sign_direct_args(coin, &input) { // If there was a `SignDirect` message in the signing input, generate the `TxRaw` directly. Ok(Some(sign_direct_args)) => ProtobufSerializer::::build_direct_signed_tx( &sign_direct_args, diff --git a/rust/tw_cosmos_sdk/src/modules/tx_builder.rs b/rust/tw_cosmos_sdk/src/modules/tx_builder.rs index 519b4b89dae..4f190f32272 100644 --- a/rust/tw_cosmos_sdk/src/modules/tx_builder.rs +++ b/rust/tw_cosmos_sdk/src/modules/tx_builder.rs @@ -4,7 +4,7 @@ use crate::address::Address; use crate::context::CosmosContext; -use crate::modules::serializer::protobuf_serializer::SignDirectArgs; +use crate::modules::serializer::protobuf_serializer::{ProtobufSerializer, SignDirectArgs}; use crate::public_key::{CosmosPublicKey, PublicKeyParams}; use crate::transaction::message::cosmos_generic_message::JsonRawMessage; use crate::transaction::message::{CosmosMessage, CosmosMessageBox}; @@ -34,16 +34,10 @@ where coin: &dyn CoinContext, input: &Proto::SigningInput<'_>, ) -> SigningResult> { - let fee = input - .fee - .as_ref() - .or_tw_err(SigningErrorType::Error_wrong_fee) - .context("No fee specified")?; let signer = Self::signer_info_from_proto(coin, input)?; - Ok(UnsignedTransaction { signer, - fee: Self::fee_from_proto(fee)?, + fee: Self::fee_from_proto(&input.fee)?, chain_id: input.chain_id.to_string(), account_number: input.account_number, tx_body: Self::tx_body_from_proto(coin, input)?, @@ -86,15 +80,20 @@ where } } - fn fee_from_proto(input: &Proto::Fee) -> SigningResult> { - let amounts = input + fn fee_from_proto(input: &Option) -> SigningResult> { + let fee_input = input + .as_ref() + .or_tw_err(SigningErrorType::Error_wrong_fee) + .context("No fee specified")?; + + let amounts = fee_input .amounts .iter() .map(Self::coin_from_proto) .collect::>()?; Ok(Fee { amounts, - gas_limit: input.gas, + gas_limit: fee_input.gas, payer: None, granter: None, }) @@ -133,6 +132,7 @@ where } pub fn try_sign_direct_args( + context: &dyn CoinContext, input: &Proto::SigningInput<'_>, ) -> SigningResult> { use Proto::mod_Message::OneOfmessage_oneof as MessageEnum; @@ -141,15 +141,30 @@ where return Ok(None); }; - match msg.message_oneof { - MessageEnum::sign_direct_message(ref direct) => Ok(Some(SignDirectArgs { - tx_body: direct.body_bytes.to_vec(), - auth_info: direct.auth_info_bytes.to_vec(), - chain_id: input.chain_id.to_string(), - account_number: input.account_number, - })), - _ => Ok(None), + let MessageEnum::sign_direct_message(ref direct) = msg.message_oneof else { + return Ok(None); + }; + + if direct.body_bytes.is_empty() { + return SigningError::err(SigningErrorType::Error_invalid_params) + .context("`body_bytes` must not be empty"); } + + let auth_info = if direct.auth_info_bytes.is_empty() { + let signer = Self::signer_info_from_proto(context, input)?; + let fee = Self::fee_from_proto(&input.fee)?; + let auth_info = ProtobufSerializer::::build_auth_info(&signer, &fee); + serialize(&auth_info)? + } else { + direct.auth_info_bytes.to_vec() + }; + + Ok(Some(SignDirectArgs { + tx_body: direct.body_bytes.to_vec(), + auth_info, + chain_id: input.chain_id.to_string(), + account_number: input.account_number, + })) } pub fn tx_message( diff --git a/rust/tw_cosmos_sdk/tests/sign.rs b/rust/tw_cosmos_sdk/tests/sign.rs index 8b703393ff3..ae3c5595f14 100644 --- a/rust/tw_cosmos_sdk/tests/sign.rs +++ b/rust/tw_cosmos_sdk/tests/sign.rs @@ -285,6 +285,45 @@ fn test_sign_direct() { }); } +/// Build and sign a transaction with `TxBody` bytes only, +/// and `AuthInfo` will be generated from `SigningInput` parameters. +#[test] +fn test_sign_direct_with_body_bytes() { + use tw_cosmos_sdk::proto::cosmos::tx::v1beta1 as tx_proto; + + let coin = TestCoinContext::default() + .with_public_key_type(PublicKeyType::Secp256k1) + .with_hrp("cosmos"); + + let body_bytes = "0a89010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412690a2d636f736d6f733168736b366a727979716a6668703564686335357463396a74636b796778306570683664643032122d636f736d6f73317a743530617a7570616e716c66616d356166687633686578777975746e756b656834633537331a090a046d756f6e120131".decode_hex().unwrap(); + + let sign_direct = Proto::mod_Message::SignDirect { + body_bytes: Cow::from(body_bytes), + // Do not specify the `AuthInfo` bytes. + auth_info_bytes: Cow::default(), + }; + let mut input = Proto::SigningInput { + account_number: 1037, + chain_id: "gaia-13003".into(), + fee: Some(make_fee(200000, make_amount("muon", "200"))), + sequence: 8, + private_key: account_1037_private_key(), + messages: vec![make_message(MessageEnum::sign_direct_message(sign_direct))], + ..Proto::SigningInput::default() + }; + + // real-world tx: https://www.mintscan.io/cosmos/txs/817101F3D96314AD028733248B28BAFAD535024D7D2C8875D3FE31DC159F096B + // curl -H 'Content-Type: application/json' --data-binary '{"tx_bytes": "Cr4BCr...1yKOU=", "mode": "BROADCAST_MODE_BLOCK"}' https://api.cosmos.network/cosmos/tx/v1beta1/txs + // also similar TX: BCDAC36B605576C8182C2829C808B30A69CAD4959D5ED1E6FF9984ABF280D603 + test_sign_protobuf::(TestInput { + coin: &coin, + input: input.clone(), + tx: r#"{"mode":"BROADCAST_MODE_BLOCK","tx_bytes":"CowBCokBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmkKLWNvc21vczFoc2s2anJ5eXFqZmhwNWRoYzU1dGM5anRja3lneDBlcGg2ZGQwMhItY29zbW9zMXp0NTBhenVwYW5xbGZhbTVhZmh2M2hleHd5dXRudWtlaDRjNTczGgkKBG11b24SATESZQpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAlcobsPzfTNVe7uqAAsndErJAjqplnyudaGB0f+R+p3FEgQKAggBGAgSEQoLCgRtdW9uEgMyMDAQwJoMGkD54fQAFlekIAnE62hZYl0uQelh/HLv0oQpCciY5Dn8H1SZFuTsrGdu41PH1Uxa4woptCELi/8Ov9yzdeEFAC9H"}"#, + signature: "f9e1f4001657a42009c4eb6859625d2e41e961fc72efd2842909c898e439fc1f549916e4ecac676ee353c7d54c5ae30a29b4210b8bff0ebfdcb375e105002f47", + signature_json: r#"[{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AlcobsPzfTNVe7uqAAsndErJAjqplnyudaGB0f+R+p3F"},"signature":"+eH0ABZXpCAJxOtoWWJdLkHpYfxy79KEKQnImOQ5/B9UmRbk7KxnbuNTx9VMWuMKKbQhC4v/Dr/cs3XhBQAvRw=="}]"#, + }); +} + #[test] fn test_sign_direct_0a90010a() { let coin = TestCoinContext::default() diff --git a/src/proto/Cosmos.proto b/src/proto/Cosmos.proto index 04384442e93..0e5dbb7ffb0 100644 --- a/src/proto/Cosmos.proto +++ b/src/proto/Cosmos.proto @@ -240,8 +240,10 @@ message Message { // For signing an already serialized transaction. Account number and chain ID must be set outside. message SignDirect { // The prepared serialized TxBody + // Required bytes body_bytes = 1; // The prepared serialized AuthInfo + // Optional. If not provided, will be generated from `SigningInput` parameters. bytes auth_info_bytes = 2; }