From 7f7aacec3b37bef9d19dac06ca9c2c7f96a118eb Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Wed, 8 Apr 2026 11:31:36 +0700 Subject: [PATCH 1/9] add secret chain --- Cargo.lock | 284 ++++++++++++++++++++++-- Cargo.toml | 11 + proto/fkms/v1/signer.proto | 24 ++ src/codec.rs | 1 + src/codec/cosmwasm_secret.rs | 418 +++++++++++++++++++++++++++++++++++ src/config/signer/local.rs | 1 + src/server/service.rs | 96 +++++++- src/signer.rs | 34 +++ src/signer/local.rs | 16 +- 9 files changed, 864 insertions(+), 21 deletions(-) create mode 100644 src/codec/cosmwasm_secret.rs diff --git a/Cargo.lock b/Cargo.lock index f124b0d..75453f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,7 +104,7 @@ dependencies = [ "k256", "once_cell", "rand 0.8.5", - "secp256k1", + "secp256k1 0.30.0", "serde", "serde_json", "serde_with", @@ -195,7 +195,7 @@ dependencies = [ "either", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", ] @@ -1039,7 +1039,7 @@ dependencies = [ "http 0.2.12", "http 1.3.1", "percent-encoding", - "sha2", + "sha2 0.10.9", "time", "tracing", ] @@ -1322,6 +1322,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + [[package]] name = "bigdecimal" version = "0.4.8" @@ -1360,6 +1366,23 @@ dependencies = [ "which", ] +[[package]] +name = "bip32" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" +dependencies = [ + "bs58", + "hmac", + "k256", + "rand_core 0.6.4", + "ripemd", + "secp256k1 0.27.0", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "bip39" version = "2.2.2" @@ -1425,6 +1448,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1475,7 +1507,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2", + "sha2 0.10.9", "tinyvec", ] @@ -1669,6 +1701,17 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest 0.10.7", +] + [[package]] name = "cmake" version = "0.1.54" @@ -1690,7 +1733,7 @@ dependencies = [ "hmac", "k256", "serde", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1706,7 +1749,7 @@ dependencies = [ "once_cell", "pbkdf2", "rand 0.8.5", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1717,14 +1760,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" dependencies = [ "base64 0.21.7", - "bech32", + "bech32 0.9.1", "bs58", "const-hex", "digest 0.10.7", "generic-array 0.14.7", "ripemd", "serde", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", ] @@ -1817,6 +1860,36 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cosmos-sdk-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" +dependencies = [ + "prost", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e74fa7a22930fe0579bef560f2d64b78415d4c47b9dd976c0635136809471d" +dependencies = [ + "bip32", + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "thiserror 1.0.69", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1926,6 +1999,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "darling" version = "0.21.3" @@ -1968,6 +2054,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "der" version = "0.7.10" @@ -2049,7 +2144,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -2130,6 +2225,19 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "2.2.0" @@ -2139,7 +2247,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -2258,7 +2366,7 @@ dependencies = [ "hmac", "p256", "rand_core 0.6.4", - "sha2", + "sha2 0.10.9", "typenum", ] @@ -2345,6 +2453,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -2411,6 +2529,7 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" name = "fkms" version = "0.1.0-alpha.1" dependencies = [ + "aes", "alloy-primitives", "alloy-signer-local", "alloy-sol-types", @@ -2419,14 +2538,21 @@ dependencies = [ "aws-config", "aws-sdk-kms", "base64 0.22.1", + "bech32 0.11.1", "bs58", + "cipher", "clap", + "cmac", + "cosmrs", + "ctr", "dirs", "hex", "hex-literal", + "hkdf", "http 1.3.1", "k256", "prost", + "rand 0.8.5", "ripemd", "rlp 0.6.1", "sea-orm", @@ -2434,6 +2560,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_with", + "sha2 0.10.9", "sha3", "thiserror 2.0.18", "tiny-keccak", @@ -2445,9 +2572,19 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", + "x25519-dalek", "xrpl-rust", ] +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "paste", +] + [[package]] name = "flume" version = "0.11.1" @@ -3247,6 +3384,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "1.9.3" @@ -3384,7 +3527,7 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2", + "sha2 0.10.9", "signature", ] @@ -4975,6 +5118,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys 0.8.2", +] + [[package]] name = "secp256k1" version = "0.30.0" @@ -4983,10 +5135,19 @@ checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys", + "secp256k1-sys 0.10.1", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -5193,6 +5354,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -5349,7 +5523,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "time", @@ -5386,7 +5560,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -5431,7 +5605,7 @@ dependencies = [ "rust_decimal", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -5474,7 +5648,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -5566,6 +5740,21 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "syn" version = "1.0.109" @@ -5660,6 +5849,51 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendermint" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.9", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-proto" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" +dependencies = [ + "bytes", + "flex-error", + "prost", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6771,6 +7005,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "xmlparser" version = "0.13.6" @@ -6809,12 +7055,12 @@ dependencies = [ "reqwless", "ripemd", "rust_decimal", - "secp256k1", + "secp256k1 0.30.0", "serde", "serde_json", "serde_repr", "serde_with", - "sha2", + "sha2 0.10.9", "strum", "strum_macros", "thiserror-no-std", diff --git a/Cargo.toml b/Cargo.toml index a3069b9..7d84cd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,17 @@ tiny-keccak = { version = "2.0.2", features = ["keccak"] } serde_bytes = "0.11.19" serde_with = { version = "3.17.0", features = ["hex"] } +cosmrs = "0.22.0" +bech32 = "0.11.0" +rand = "0.8.5" +hkdf = "0.12.4" +sha2 = "0.10.8" +x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } +aes = "0.8.4" +cmac = "0.7.2" +ctr = "0.9.0" +cipher = "0.4.4" + [build-dependencies] tonic-build = { version = "0.13.1", features = ["prost"] } diff --git a/proto/fkms/v1/signer.proto b/proto/fkms/v1/signer.proto index 949bc6f..5b8220c 100644 --- a/proto/fkms/v1/signer.proto +++ b/proto/fkms/v1/signer.proto @@ -6,6 +6,7 @@ service FkmsService { rpc SignEvm(SignEvmRequest) returns (SignEvmResponse); rpc SignXrpl(SignXrplRequest) returns (SignXrplResponse); rpc SignIcon(SignIconRequest) returns (SignIconResponse); + rpc SignSecret(SignSecretRequest) returns (SignSecretResponse); rpc GetSignerAddresses(GetSignerAddressesRequest) returns (GetSignerAddressesResponse); } @@ -36,6 +37,15 @@ message SignIconResponse { bytes tx_params = 1; } +message SignSecretRequest { + SecretSignerPayload signer_payload = 1; + Tss tss = 2; +} + +message SignSecretResponse { + bytes tx_blob = 1; +} + message GetSignerAddressesRequest {} message GetSignerAddressesResponse { @@ -56,6 +66,19 @@ message IconSignerPayload { string network_id = 4; } +message SecretSignerPayload { + string sender = 1; + string contract_address = 2; + string chain_id = 3; + uint64 account_number = 4; + uint64 sequence = 5; + uint64 gas_limit = 6; + string gas_prices = 7; + string memo = 8; + string code_hash = 9; + string pubkey = 10; +} + message Tss { bytes message = 1; bytes random_addr = 2; @@ -71,4 +94,5 @@ enum ChainType { EVM = 0; XRPL = 1; ICON = 2; + SECRET = 3; } diff --git a/src/codec.rs b/src/codec.rs index 028ef5d..a9df5cb 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -1,3 +1,4 @@ +pub mod cosmwasm_secret; pub mod evm; pub mod icon; pub mod tss; diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs new file mode 100644 index 0000000..52d5620 --- /dev/null +++ b/src/codec/cosmwasm_secret.rs @@ -0,0 +1,418 @@ +use anyhow::{Context, Result, anyhow}; +use cmac::{Cmac, Mac}; +use cosmrs::{ + AccountId, Any, + crypto::secp256k1, + tx::{Body, Fee, SignDoc, SignerInfo}, +}; +use hkdf::Hkdf; +use prost::Message; +use rand::RngCore; +use serde_json::json; +use sha2::Sha256; +use std::str; +use x25519_dalek::{PublicKey, StaticSecret}; + +use aes::Aes128; +use ctr::Ctr128BE; +use ctr::cipher::{KeyIvInit, StreamCipher}; + +const HKDF_SALT: [u8; 32] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x4b, 0xea, 0xd8, 0xdf, 0x69, 0x99, + 0x08, 0x52, 0xc2, 0x02, 0xdb, 0x0e, 0x00, 0x97, 0xc1, 0xa1, 0x2e, 0xa6, 0x37, 0xd7, 0xe9, 0x6d, +]; + +// Secret compute uses this exact type url for MsgExecuteContract. +const SECRET_MSG_EXECUTE_CONTRACT_TYPE_URL: &str = "/secret.compute.v1beta1.MsgExecuteContract"; + +// ---- Protobuf types (minimal subset) ---- + +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Coin { + #[prost(string, tag = "1")] + pub denom: String, + #[prost(string, tag = "2")] + pub amount: String, +} + +// Matches: secret.compute.v1beta1.MsgExecuteContract +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MsgExecuteContract { + // sender sdk.AccAddress (20 bytes) + #[prost(bytes, tag = "1")] + pub sender: Vec, + #[prost(bytes, tag = "2")] + pub contract: Vec, + // encrypted bytes (nonce||pubkey||ciphertext) + #[prost(bytes, tag = "3")] + pub msg: Vec, + // optional on chain; empty is fine + #[prost(string, tag = "4")] + pub callback_code_hash: String, + // repeated sent_funds; empty for oracle relays + #[prost(message, repeated, tag = "5")] + pub sent_funds: Vec, + #[prost(bytes, tag = "6")] + pub callback_sig: Vec, +} + +// ---- Public helpers used by fkms service.rs ---- + +pub fn secret_execute_msg_json( + symbols: Vec, + rates: Vec, + resolve_time: u64, + request_id: u64, +) -> Result> { + let rates_str = rates.iter().map(|r| r.to_string()).collect::>(); + let obj = json!({ + "relay": { + "symbols": symbols, + "rates": rates_str, + "resolve_time": resolve_time, + "request_id": request_id, + } + }); + + serde_json::to_vec(&obj).context("failed to serialize secret execute msg json") +} + +pub fn decode_acc_address_bech32(addr: &str) -> Result> { + let account_id: AccountId = addr + .parse() + .map_err(|e| anyhow!("failed to parse bech32 account id: {e}"))?; + let bytes = account_id.to_bytes(); + if bytes.len() != 20 { + return Err(anyhow!( + "invalid account address length: expected 20 bytes, got {}", + bytes.len() + )); + } + Ok(bytes) +} + +pub fn encrypt_secret_execute_msg( + code_hash: &str, + encryption_pubkey_hex: &str, + execute_msg_json: &[u8], +) -> Result> { + let encryption_pubkey_hex = encryption_pubkey_hex.trim_start_matches("0x"); + let receiver_pubkey_bytes = + hex::decode(encryption_pubkey_hex).with_context(|| "invalid hex pubkey")?; + if receiver_pubkey_bytes.len() != 32 { + return Err(anyhow!( + "invalid x25519 pubkey length: expected 32, got {}", + receiver_pubkey_bytes.len() + )); + } + let receiver_pubkey = PublicKey::from( + <[u8; 32]>::try_from(receiver_pubkey_bytes.as_slice()) + .map_err(|_| anyhow!("pubkey should be 32 bytes"))?, + ); + + let mut plaintext = Vec::with_capacity(code_hash.len() + execute_msg_json.len()); + plaintext.extend_from_slice(code_hash.as_bytes()); + plaintext.extend_from_slice(execute_msg_json); + + offline_encrypt_secret_message(&plaintext, &receiver_pubkey) +} + +pub fn sign_secret_tx( + signer_private_key: &[u8], + sender_address_bech32: &str, + contract_address_bech32: &str, + encrypted_execute_msg: Vec, + chain_id: &str, + account_number: u64, + sequence: u64, + gas_limit: u64, + gas_prices: &str, + memo: &str, +) -> Result> { + let sender_bytes = decode_acc_address_bech32(sender_address_bech32)?; + let contract_bytes = decode_acc_address_bech32(contract_address_bech32)?; + + // Build secret MsgExecuteContract protobuf bytes. + let msg = MsgExecuteContract { + sender: sender_bytes, + contract: contract_bytes, + msg: encrypted_execute_msg, + callback_code_hash: "".to_string(), + sent_funds: vec![], + callback_sig: vec![], + }; + let msg_bytes = msg.encode_to_vec(); + + let msg_any = Any { + type_url: SECRET_MSG_EXECUTE_CONTRACT_TYPE_URL.to_string(), + value: msg_bytes, + }; + + let tx_body = Body::new(vec![msg_any], memo.to_string(), 0u16); + + let fee_coin = parse_gas_prices_to_fee_coin(gas_prices, gas_limit)?; + let signing_key = secp256k1::SigningKey::from_slice(signer_private_key) + .map_err(|e| anyhow!("invalid signer private key: {e}"))?; + + let signer_info = SignerInfo::single_direct(Some(signing_key.public_key()), sequence); + + let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(fee_coin, gas_limit)); + + let chain_id = chain_id + .parse() + .map_err(|e| anyhow!("invalid chain_id {chain_id}: {e}"))?; + + let sign_doc = SignDoc::new(&tx_body, &auth_info, &chain_id, account_number) + .map_err(|e| anyhow!("failed to create SignDoc: {e}"))?; + let tx_signed = sign_doc + .sign(&signing_key) + .map_err(|e| anyhow!("failed to sign SignDoc: {e}"))?; + + tx_signed + .to_bytes() + .map_err(|e| anyhow!("failed to serialize signed tx: {e}")) +} + +fn offline_encrypt_secret_message( + plaintext: &[u8], + receiver_pubkey: &PublicKey, +) -> Result> { + // 1) Generate txSender priv/pub + // 2) Generate nonce(32) + // 3) hkdf(txEncryptionIkm = X25519(txSenderPrivKey, receiverPubKey) || nonce, hkdfSalt) + // 4) encryptData(AES-CMAC-SIV, aesEncryptionKey, txSenderPubKey, plaintext, nonce) + // 5) output = nonce(32) || txSenderPubKey(32) || ciphertext + + let mut tx_sender_privkey = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut tx_sender_privkey); + let tx_sender_static = StaticSecret::from(tx_sender_privkey); + let tx_sender_pubkey = PublicKey::from(&tx_sender_static); + + let mut nonce = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut nonce); + + let tx_encryption_ikm = tx_sender_static.diffie_hellman(receiver_pubkey); + + let mut ikm = Vec::with_capacity(32 + 32); + ikm.extend_from_slice(tx_encryption_ikm.as_bytes()); + ikm.extend_from_slice(&nonce); + + let hk = Hkdf::::new(Some(&HKDF_SALT), &ikm); + let mut okm = [0u8; 32]; + hk.expand(&[], &mut okm) + .map_err(|e| anyhow!("HKDF expand failed: {e}"))?; + + let aes_encryption_key = okm; // 32 bytes + + let ciphertext = encrypt_data_cmac_siv(&aes_encryption_key, plaintext)?; + + let mut out = Vec::with_capacity(32 + 32 + ciphertext.len()); + out.extend_from_slice(&nonce); + out.extend_from_slice(tx_sender_pubkey.as_bytes()); + out.extend_from_slice(&ciphertext); + Ok(out) +} + +fn encrypt_data_cmac_siv(aes_encryption_key: &[u8; 32], plaintext: &[u8]) -> Result> { + let (cmac_key, ctr_key) = aes_encryption_key.split_at(16); + let cmac_key: &[u8; 16] = cmac_key.try_into().expect("16 bytes"); + let ctr_key: &[u8; 16] = ctr_key.try_into().expect("16 bytes"); + + // Associated data items list contains a single empty slice. + let associated_data_items: Vec<&[u8]> = vec![&[]]; + let siv_tag = s2v_aes_cmac_siv(cmac_key, &associated_data_items, plaintext)?; + + // SIV => CTR IV with top bits cleared in last two 32-bit words. + let mut ctr_iv = siv_tag; + zero_iv_bits(&mut ctr_iv); + + // Encrypt with AES-CTR, producing ciphertext = siv_tag || encrypted(plaintext) + let mut buf = plaintext.to_vec(); + let mut stream_cipher = Ctr128BE::::new_from_slices(ctr_key, &ctr_iv) + .map_err(|e| anyhow!("failed to create AES-CTR cipher: {e}"))?; + stream_cipher.apply_keystream(&mut buf); + + let mut out = Vec::with_capacity(16 + buf.len()); + out.extend_from_slice(&siv_tag); + out.extend_from_slice(&buf); + + Ok(out) +} + +fn zero_iv_bits(iv: &mut [u8; 16]) { + // "We zero-out the top bit in each of the last two 32-bit words of the IV" + // — http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf + iv[16 - 8] &= 0x7f; + iv[16 - 4] &= 0x7f; +} + +fn s2v_aes_cmac_siv( + cmac_key: &[u8; 16], + associated_data_items: &[&[u8]], + plaintext: &[u8], +) -> Result<[u8; 16]> { + // Port of miscreant.go's Cipher.s2v for AES-CMAC-SIV-256. + // block size is 16. + let block_size = 16usize; + let zeros = [0u8; 16]; + let mut d = cmac_digest(cmac_key, &zeros)?; + + for v in associated_data_items { + let tmp = cmac_digest(cmac_key, v)?; + d = dbl_128(&d); + xor_inplace(&mut d, &tmp); + } + + if plaintext.len() >= block_size { + let n = plaintext.len() - block_size; + let mut mac = cmac_init(cmac_key)?; + mac.update(&plaintext[..n]); + + let mut tmp = [0u8; 16]; + tmp.copy_from_slice(&plaintext[n..]); + xor_inplace(&mut tmp, &d); + mac.update(&tmp); + + let result = mac.finalize().into_bytes(); + Ok(result.into()) + } else { + let mut tmp = [0u8; 16]; + tmp[..plaintext.len()].copy_from_slice(plaintext); + tmp[plaintext.len()] = 0x80; + let d2 = dbl_128(&d); + xor_inplace(&mut tmp, &d2); + + let mut mac = cmac_init(cmac_key)?; + mac.update(&tmp); + let result = mac.finalize().into_bytes(); + Ok(result.into()) + } +} + +fn xor_inplace(a: &mut [u8; 16], b: &[u8; 16]) { + for i in 0..16 { + a[i] ^= b[i]; + } +} + +fn dbl_128(d: &[u8; 16]) -> [u8; 16] { + // CMAC subkey doubling in GF(2^128) with Rb=0x87. + let msb = (d[0] & 0x80) != 0; + let mut out = [0u8; 16]; + + let mut carry = 0u8; + for i in (0..16).rev() { + out[i] = (d[i] << 1) | carry; + carry = if (d[i] & 0x80) != 0 { 1 } else { 0 }; + } + + if msb { + out[15] ^= 0x87; + } + out +} + +fn cmac_init(key: &[u8; 16]) -> Result> { + let mac = Cmac::::new_from_slice(key).map_err(|e| anyhow!("CMAC init failed: {e}"))?; + Ok(mac) +} + +fn cmac_digest(key: &[u8; 16], data: &[u8]) -> Result<[u8; 16]> { + let mut mac = cmac_init(key)?; + mac.update(data); + let result = mac.finalize().into_bytes(); + Ok(result.into()) +} + +fn parse_gas_prices_to_fee_coin(gas_prices: &str, gas_limit: u64) -> Result { + let gas_prices = gas_prices.trim(); + if gas_prices.is_empty() { + return Err(anyhow!("gas_prices is empty")); + } + + // Split numeric part vs denom part. + let mut idx = 0usize; + for (i, ch) in gas_prices.char_indices() { + if ch.is_ascii_digit() || ch == '.' { + idx = i + ch.len_utf8(); + } else { + break; + } + } + if idx == 0 || idx >= gas_prices.len() { + return Err(anyhow!("invalid gas_prices format: {gas_prices}")); + } + let (num_str, denom) = gas_prices.split_at(idx); + let denom = denom.to_string(); + let num_str = num_str; + + let (int_part, frac_part) = if let Some(dot) = num_str.find('.') { + (&num_str[..dot], &num_str[dot + 1..]) + } else { + (num_str, "") + }; + + let scale: u128 = if frac_part.is_empty() { + 1 + } else { + 10u128.pow(frac_part.len() as u32) + }; + + let int_part_val: u128 = if int_part.is_empty() { + 0 + } else { + int_part.parse()? + }; + let frac_part_val: u128 = if frac_part.is_empty() { + 0 + } else { + frac_part.parse()? + }; + + let amount_scaled = int_part_val + .checked_mul(scale) + .ok_or_else(|| anyhow!("gas_prices amount overflow"))? + .checked_add(frac_part_val) + .ok_or_else(|| anyhow!("gas_prices amount overflow"))?; + + let fee_scaled = amount_scaled + .checked_mul(gas_limit as u128) + .ok_or_else(|| anyhow!("fee overflow"))?; + let fee_amount = fee_scaled / scale; + + let coin = cosmrs::Coin { + denom: denom + .parse() + .map_err(|e| anyhow!("invalid denom in gas_prices: {e}"))?, + amount: fee_amount, + }; + + Ok(coin) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_acc_address_bech32_length() { + // This is just to ensure decoding/length logic works with a sample; + // address correctness is not asserted here. + // Replace with a real Secret Network address if needed. + let _ = decode_acc_address_bech32("secret1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3d4r"); // should error + } + + #[test] + fn test_secret_execute_msg_json_shape() { + let symbols = vec!["BTC".to_string(), "ETH".to_string()]; + let rates = vec![67758920310332u64, 1410834569u64]; + let json_bz = secret_execute_msg_json(symbols, rates, 10, 20).unwrap(); + let s = str::from_utf8(&json_bz).unwrap(); + assert!(s.contains("\"relay\"")); + assert!(s.contains("\"symbols\"")); + assert!(s.contains("\"rates\"")); + // resolve_time and request_id must be JSON numbers, not strings + assert!(s.contains("\"resolve_time\":10")); + assert!(s.contains("\"request_id\":20")); + } +} diff --git a/src/config/signer/local.rs b/src/config/signer/local.rs index d7575ef..ab33852 100644 --- a/src/config/signer/local.rs +++ b/src/config/signer/local.rs @@ -30,4 +30,5 @@ pub enum ChainType { Evm, Xrpl, Icon, + Secret, } diff --git a/src/server/service.rs b/src/server/service.rs index bd7ba07..39d5b1c 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -1,3 +1,6 @@ +use crate::codec::cosmwasm_secret::{ + encrypt_secret_execute_msg, secret_execute_msg_json, sign_secret_tx, +}; use crate::codec::evm::decode_tx; use crate::codec::icon::{ create_signing_payload as create_icon_signing_payload, encode_tx_for_signing, sign_tx, @@ -10,7 +13,8 @@ use crate::proto::fkms::v1::ChainType as proto_chain_type; use crate::proto::fkms::v1::fkms_service_server::FkmsService; use crate::proto::fkms::v1::{ GetSignerAddressesRequest, GetSignerAddressesResponse, SignEvmRequest, SignEvmResponse, - SignIconRequest, SignIconResponse, SignXrplRequest, SignXrplResponse, Signers, + SignIconRequest, SignIconResponse, SignSecretRequest, SignSecretResponse, SignXrplRequest, + SignXrplResponse, Signers, }; use crate::server::Server; use crate::server::utils::filter_usd_signal; @@ -252,6 +256,95 @@ impl FkmsService for Server { } } + #[instrument(skip(self, request))] + async fn sign_secret( + &self, + request: Request, + ) -> Result, Status> { + let sign_secret_request = request.into_inner(); + + let signer_payload = sign_secret_request.signer_payload.ok_or_else(|| { + Status::invalid_argument("signer_payload field is required and cannot be null") + })?; + let tss = sign_secret_request + .tss + .ok_or_else(|| Status::invalid_argument("tss field is required and cannot be null"))?; + + // verify tss signature + if let Some(verifier) = &self.tss_signature_verifier { + verifier + .verify_signature(&tss.message, &tss.random_addr, &tss.signature_s) + .map_err(|e| { + error!("failed to verify tss message: {:?}", e); + Status::invalid_argument(format!("Failed to verify tss signature: {e}")) + })?; + } + + let decoded_tss_message = decode_tss_message(&tss.message) + .map_err(|e| Status::internal(format!("Failed to decode TSS message: {e}")))?; + let tunnel_packet = decoded_tss_message.packet; + + for hook in &self.pre_sign_hooks { + hook.call(&tunnel_packet).await?; + } + + match self + .signers + .get(&(ChainType::Secret, signer_payload.sender.clone())) + { + Some(signer) => { + let signals: Vec<(String, u64)> = tunnel_packet + .signals + .iter() + .filter_map(filter_usd_signal) + .collect(); + let symbols: Vec = signals.iter().map(|(sym, _)| sym.clone()).collect(); + let rates: Vec = signals.iter().map(|(_, rate)| *rate).collect(); + + let resolve_time = u64::try_from(tunnel_packet.timestamp) + .map_err(|_| Status::invalid_argument("Timestamp must be non-negative"))?; + let request_id = tunnel_packet.sequence; + + let execute_msg_json = + secret_execute_msg_json(symbols, rates, resolve_time, request_id).map_err( + |e| Status::internal(format!("Failed to build execute msg json: {e}")), + )?; + + let encrypted_execute_msg = encrypt_secret_execute_msg( + &signer_payload.code_hash, + &signer_payload.pubkey, + &execute_msg_json, + ) + .map_err(|e| Status::internal(format!("Failed to encrypt execute msg: {e}")))?; + + let pk_bytes = signer.private_key().ok_or_else(|| { + Status::invalid_argument("Signer private key is required for Secret signing") + })?; + + let tx_blob = sign_secret_tx( + pk_bytes, + &signer_payload.sender, + &signer_payload.contract_address, + encrypted_execute_msg, + &signer_payload.chain_id, + signer_payload.account_number, + signer_payload.sequence, + signer_payload.gas_limit, + &signer_payload.gas_prices, + &signer_payload.memo, + ) + .map_err(|e| Status::internal(format!("Failed to sign Secret tx: {e}")))?; + + info!("successfully signed secret cosmwasm message"); + Ok(Response::new(SignSecretResponse { tx_blob })) + } + None => { + warn!("no signer found for {}", signer_payload.sender); + Err(Status::not_found("Signer not found")) + } + } + } + #[instrument(skip(self, _request))] async fn get_signer_addresses( &self, @@ -274,6 +367,7 @@ impl FkmsService for Server { ChainType::Evm => proto_chain_type::Evm, ChainType::Xrpl => proto_chain_type::Xrpl, ChainType::Icon => proto_chain_type::Icon, + ChainType::Secret => proto_chain_type::Secret, }; Signers { chain_type: proto_ct as i32, diff --git a/src/signer.rs b/src/signer.rs index 982e320..c36bd0b 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -11,6 +11,7 @@ use ripemd::Ripemd160; use sha3::Sha3_256; use crate::config::signer::local::ChainType; +use cosmrs::AccountId; #[async_trait::async_trait] pub trait Signer: Send + Sync + 'static { @@ -23,6 +24,11 @@ pub trait Signer: Send + Sync + 'static { fn address(&self) -> &str; fn chain_type(&self) -> &ChainType; + + // For remote signers (AWS/HSM), this is left as `None`. + fn private_key(&self) -> Option<&[u8]> { + None + } } pub fn public_key_to_evm_address(public_key: &[u8]) -> anyhow::Result { @@ -97,3 +103,31 @@ pub fn public_key_to_icon_address(public_key: &[u8]) -> anyhow::Result { // Icon address is the last 20 bytes of the Sha3_256 hash Ok(format!("hx{}", hex::encode(&hash[12..]))) } + +pub fn public_key_to_secret_address(public_key: &[u8]) -> anyhow::Result { + // Cosmos SDK account address for secp256k1 is: + // bech32(prefix, ripemd160(sha256(pubkey_bytes))) + // where pubkey_bytes is the compressed SEC1 encoded pubkey (33 bytes). + if public_key.len() != 33 { + return Err(anyhow!( + "Invalid public key length for Secret address. Expected 33 bytes, got {}", + public_key.len() + )); + } + + // For compressed SEC1 pubkeys, the first byte is 0x02 or 0x03. + if public_key[0] != 0x02 && public_key[0] != 0x03 { + return Err(anyhow!( + "Invalid public key SEC1 prefix for Secret address. Expected 0x02/0x03, got 0x{:02x}", + public_key[0] + )); + } + + let sha256 = Sha256::digest(public_key); + let account_id = Ripemd160::digest(sha256); + let acc_id_bytes: &[u8] = account_id.as_slice(); + // Secret Network typically uses the `secret` bech32 prefix. + let account_id = AccountId::new("secret", acc_id_bytes) + .map_err(|e| anyhow!("failed to create Secret AccountId: {e}"))?; + Ok(account_id.to_string()) +} diff --git a/src/signer/local.rs b/src/signer/local.rs index f0c25c9..e9838df 100644 --- a/src/signer/local.rs +++ b/src/signer/local.rs @@ -2,7 +2,8 @@ use crate::config::signer::local::ChainType; use crate::signer::signature::Signature; use crate::signer::signature::ecdsa::DerSignature; use crate::signer::{ - Signer, public_key_to_evm_address, public_key_to_icon_address, public_key_to_xrpl_address, + Signer, public_key_to_evm_address, public_key_to_icon_address, public_key_to_secret_address, + public_key_to_xrpl_address, }; use k256::ecdsa::SigningKey as EcdsaSigningKey; use k256::ecdsa::signature::hazmat::PrehashSigner; @@ -11,6 +12,7 @@ use k256::ecdsa::signature::hazmat::PrehashSigner; pub struct LocalSigner { signing_key: SigningKey, public_key: Vec, + private_key: Vec, address: String, chain_type: ChainType, } @@ -40,11 +42,18 @@ impl LocalSigner { let address = public_key_to_icon_address(&public_key)?; (SigningKey::Ecdsa(signing_key), public_key, address) } + ChainType::Secret => { + let signing_key = EcdsaSigningKey::from_slice(private_key)?; + let public_key = create_public_key(&signing_key, true); + let address = public_key_to_secret_address(&public_key)?; + (SigningKey::Ecdsa(signing_key), public_key, address) + } }; Ok(LocalSigner { signing_key, public_key, + private_key: private_key.to_vec(), address, chain_type: chain_type.clone(), }) @@ -75,6 +84,7 @@ impl Signer for LocalSigner { ChainType::Evm => self.sign_ecdsa(message).await, ChainType::Xrpl => self.sign_der(message).await, ChainType::Icon => self.sign_ecdsa(message).await, + ChainType::Secret => self.sign_ecdsa(message).await, } } @@ -89,6 +99,10 @@ impl Signer for LocalSigner { fn chain_type(&self) -> &ChainType { &self.chain_type } + + fn private_key(&self) -> Option<&[u8]> { + Some(&self.private_key) + } } fn create_public_key(signing_key: &EcdsaSigningKey, compressed: bool) -> Vec { From 7358b9a02bd1d5329d2a5cb6ce055ae1c4adf00c Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Wed, 8 Apr 2026 13:35:40 +0700 Subject: [PATCH 2/9] update rust workflows version --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dfe2e8..1c4dba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.93 + toolchain: 1.94 components: clippy - name: Install protoc run: | @@ -39,7 +39,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.93 + toolchain: 1.94 - name: Install protoc run: | sudo apt-get update From 18782abb3f82c1c5052a163fb9d7954caa7c2cd9 Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Wed, 8 Apr 2026 13:51:08 +0700 Subject: [PATCH 3/9] fix clippy --- src/codec/cosmwasm_secret.rs | 50 +++++++++++++++++++----------------- src/server/service.rs | 28 ++++++++++---------- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs index 52d5620..5869ea7 100644 --- a/src/codec/cosmwasm_secret.rs +++ b/src/codec/cosmwasm_secret.rs @@ -117,26 +117,28 @@ pub fn encrypt_secret_execute_msg( offline_encrypt_secret_message(&plaintext, &receiver_pubkey) } -pub fn sign_secret_tx( - signer_private_key: &[u8], - sender_address_bech32: &str, - contract_address_bech32: &str, - encrypted_execute_msg: Vec, - chain_id: &str, - account_number: u64, - sequence: u64, - gas_limit: u64, - gas_prices: &str, - memo: &str, -) -> Result> { - let sender_bytes = decode_acc_address_bech32(sender_address_bech32)?; - let contract_bytes = decode_acc_address_bech32(contract_address_bech32)?; +pub struct SignSecretTxParams { + pub signer_private_key: Vec, + pub sender_address_bech32: String, + pub contract_address_bech32: String, + pub encrypted_execute_msg: Vec, + pub chain_id: String, + pub account_number: u64, + pub sequence: u64, + pub gas_limit: u64, + pub gas_prices: String, + pub memo: String, +} + +pub fn sign_secret_tx(params: SignSecretTxParams) -> Result> { + let sender_bytes = decode_acc_address_bech32(¶ms.sender_address_bech32)?; + let contract_bytes = decode_acc_address_bech32(¶ms.contract_address_bech32)?; // Build secret MsgExecuteContract protobuf bytes. let msg = MsgExecuteContract { sender: sender_bytes, contract: contract_bytes, - msg: encrypted_execute_msg, + msg: params.encrypted_execute_msg, callback_code_hash: "".to_string(), sent_funds: vec![], callback_sig: vec![], @@ -148,21 +150,22 @@ pub fn sign_secret_tx( value: msg_bytes, }; - let tx_body = Body::new(vec![msg_any], memo.to_string(), 0u16); + let tx_body = Body::new(vec![msg_any], params.memo, 0u16); - let fee_coin = parse_gas_prices_to_fee_coin(gas_prices, gas_limit)?; - let signing_key = secp256k1::SigningKey::from_slice(signer_private_key) + let fee_coin = parse_gas_prices_to_fee_coin(¶ms.gas_prices, params.gas_limit)?; + let signing_key = secp256k1::SigningKey::from_slice(¶ms.signer_private_key) .map_err(|e| anyhow!("invalid signer private key: {e}"))?; - let signer_info = SignerInfo::single_direct(Some(signing_key.public_key()), sequence); + let signer_info = SignerInfo::single_direct(Some(signing_key.public_key()), params.sequence); - let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(fee_coin, gas_limit)); + let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(fee_coin, params.gas_limit)); - let chain_id = chain_id + let chain_id = params + .chain_id .parse() - .map_err(|e| anyhow!("invalid chain_id {chain_id}: {e}"))?; + .map_err(|e| anyhow!("invalid chain_id {}: {e}", params.chain_id))?; - let sign_doc = SignDoc::new(&tx_body, &auth_info, &chain_id, account_number) + let sign_doc = SignDoc::new(&tx_body, &auth_info, &chain_id, params.account_number) .map_err(|e| anyhow!("failed to create SignDoc: {e}"))?; let tx_signed = sign_doc .sign(&signing_key) @@ -344,7 +347,6 @@ fn parse_gas_prices_to_fee_coin(gas_prices: &str, gas_limit: u64) -> Result Date: Wed, 8 Apr 2026 15:10:29 +0700 Subject: [PATCH 4/9] add test --- Cargo.lock | 10 +---- Cargo.toml | 2 - src/codec/cosmwasm_secret.rs | 80 ++++++++++++++++++++++++++++++++---- src/server/service.rs | 5 ++- src/signer/local.rs | 54 +++++++++++++++++++++++- 5 files changed, 131 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ff8b98..9c4b128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1297,12 +1297,6 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" -[[package]] -name = "bech32" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" - [[package]] name = "bigdecimal" version = "0.4.10" @@ -1704,7 +1698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" dependencies = [ "base64 0.21.7", - "bech32 0.9.1", + "bech32", "bs58", "const-hex", "digest 0.10.7", @@ -2512,10 +2506,8 @@ dependencies = [ "aws-sdk-kms", "base32", "base64 0.22.1", - "bech32 0.11.1", "bip39", "bs58", - "cipher", "clap", "cmac", "cosmrs", diff --git a/Cargo.toml b/Cargo.toml index 5a956fe..25bc0f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,6 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus base32 = "0.5.1" cosmrs = "0.22.0" -bech32 = "0.11.0" rand = "0.8.5" hkdf = "0.12.4" sha2 = "0.10.8" @@ -67,7 +66,6 @@ x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } aes = "0.8.4" cmac = "0.7.2" ctr = "0.9.0" -cipher = "0.4.4" [build-dependencies] tonic-build = { version = "0.13.1", features = ["prost"] } diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs index 5869ea7..d706776 100644 --- a/src/codec/cosmwasm_secret.rs +++ b/src/codec/cosmwasm_secret.rs @@ -110,8 +110,17 @@ pub fn encrypt_secret_execute_msg( .map_err(|_| anyhow!("pubkey should be 32 bytes"))?, ); - let mut plaintext = Vec::with_capacity(code_hash.len() + execute_msg_json.len()); - plaintext.extend_from_slice(code_hash.as_bytes()); + let code_hash_hex = code_hash.trim_start_matches("0x"); + let code_hash_bytes = hex::decode(code_hash_hex).context("invalid code_hash hex")?; + if code_hash_bytes.len() != 32 { + return Err(anyhow!( + "invalid code_hash length: expected 32 bytes (64 hex chars), got {}", + code_hash_bytes.len() + )); + } + + let mut plaintext = Vec::with_capacity(code_hash_bytes.len() + execute_msg_json.len()); + plaintext.extend_from_slice(&code_hash_bytes); plaintext.extend_from_slice(execute_msg_json); offline_encrypt_secret_message(&plaintext, &receiver_pubkey) @@ -380,7 +389,7 @@ fn parse_gas_prices_to_fee_coin(gas_prices: &str, gas_limit: u64) -> Result should round up to 1 + let coin = parse_gas_prices_to_fee_coin("0.025uscrt", 10).unwrap(); + assert_eq!(coin.amount, 1); + assert_eq!(coin.denom.to_string(), "uscrt"); + + // 0.001 * 500 = 0.5 -> should round up to 1 + let coin = parse_gas_prices_to_fee_coin("0.001uscrt", 500).unwrap(); + assert_eq!(coin.amount, 1); + + // 0.15 * 10 = 1.5 -> should round up to 2 + let coin = parse_gas_prices_to_fee_coin("0.15uscrt", 10).unwrap(); + assert_eq!(coin.amount, 2); + + // Exact integer case: 0.1 * 10 = 1.0 -> should be 1 + let coin = parse_gas_prices_to_fee_coin("0.1uscrt", 10).unwrap(); + assert_eq!(coin.amount, 1); + } + + #[test] + fn test_encrypt_secret_execute_msg_code_hash_handling() { + let code_hash = "0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; + let pubkey_hex = "415082ba441584c017f8a75e3328229e64a4d656f4e1f727bcaadc3b7e71612a"; + let msg = b"{}"; + + // Should succeed with 0x prefix + let result = encrypt_secret_execute_msg(code_hash, pubkey_hex, msg); + assert!(result.is_ok(), "failed with 0x prefix: {:?}", result.err()); + + // Should succeed without 0x prefix + let result = encrypt_secret_execute_msg(&code_hash[2..], pubkey_hex, msg); + assert!( + result.is_ok(), + "failed without 0x prefix: {:?}", + result.err() + ); + + // Should fail with invalid hex (odd number of characters) + let result = encrypt_secret_execute_msg("invalidhex", pubkey_hex, msg); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("invalid code_hash hex") + ); + + // Should fail with invalid length (even characters, but not 32 bytes) + let result = encrypt_secret_execute_msg("deadbeef", pubkey_hex, msg); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("invalid code_hash length") + ); + } } diff --git a/src/server/service.rs b/src/server/service.rs index 623d12a..5175b19 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -535,7 +535,10 @@ impl FkmsService for Server { .map_err(|e| Status::internal(format!("Failed to encrypt execute msg: {e}")))?; let pk_bytes = signer.private_key().ok_or_else(|| { - Status::invalid_argument("Signer private key is required for Secret signing") + Status::failed_precondition(format!( + "Secret signer is missing a private key for sender {}", + signer_payload.sender + )) })?; let secret_params = SignSecretTxParams { diff --git a/src/signer/local.rs b/src/signer/local.rs index 24e0a45..7e78130 100644 --- a/src/signer/local.rs +++ b/src/signer/local.rs @@ -212,7 +212,7 @@ mod test { use super::*; use crate::signer::Signer; use base32; - use k256::sha2::{Digest, Sha512}; + use k256::sha2::{Digest, Sha256, Sha512}; use sha3::Keccak256; #[tokio::test] @@ -373,4 +373,56 @@ mod test { let expected = "hx8521060f28fdedcc4e4544ee499008809d4c0322"; assert_eq!(address, expected); } + + #[test] + fn test_public_key_to_secret_address() { + let pk = hex::decode("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f") + .unwrap(); + let address = public_key_to_secret_address(&pk).unwrap(); + let expected = "secret10xcqpzrky6eff2g52qdye53xkk9jxkvrr9w4al"; + assert_eq!(address, expected); + } + + #[test] + fn test_address_generation_secret() { + // Known test vector for secp256k1 (private key: 0x01*32) + let pk = hex::decode("0101010101010101010101010101010101010101010101010101010101010101") + .unwrap(); + let signer = LocalSigner::new(&pk, &ChainType::Secret, None).unwrap(); + + // Verify address derivation (Cosmos style with 'secret' prefix) + // PK 0x01... -> Pubkey 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f + // Ripemd160(Sha256(Pubkey)) -> 0021c25805dc8f352e698188619bc16940866509 + // Bech32("secret", ...) -> secret10xcqpzrky6eff2g52qdye53xkk9jxkvrr9w4al + assert_eq!( + signer.address(), + "secret10xcqpzrky6eff2g52qdye53xkk9jxkvrr9w4al" + ); + + // Validate public key is compressed (33 bytes) + let pubkey = signer.public_key(); + assert_eq!( + pubkey.len(), + 33, + "Secret public key must be compressed (33 bytes)" + ); + assert!( + pubkey[0] == 0x02 || pubkey[0] == 0x03, + "Secret public key must have compressed prefix (0x02 or 0x03)" + ); + } + + #[tokio::test] + async fn test_sign_ecdsa_secret() { + let pk = hex::decode("0101010101010101010101010101010101010101010101010101010101010101") + .unwrap(); + let signer = LocalSigner::new(&pk, &ChainType::Secret, None).unwrap(); + let message = b"Hello, Secret Network!"; + // use Sha256 for Cosmos/Secret pre-hashing + let digest = Sha256::digest(message); + let signature = signer.sign(&digest).await.unwrap(); + + // Secp256k1 recoverable signature (R || S || V) should be 65 bytes + assert_eq!(signature.len(), 65); + } } From 571d4bb0175ae4aa94bdbc48a6bae7fab961dd58 Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Wed, 8 Apr 2026 16:50:57 +0700 Subject: [PATCH 5/9] fix encrypt_data_cmac_siv --- src/codec/cosmwasm_secret.rs | 42 ++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs index d706776..784ddad 100644 --- a/src/codec/cosmwasm_secret.rs +++ b/src/codec/cosmwasm_secret.rs @@ -189,12 +189,6 @@ fn offline_encrypt_secret_message( plaintext: &[u8], receiver_pubkey: &PublicKey, ) -> Result> { - // 1) Generate txSender priv/pub - // 2) Generate nonce(32) - // 3) hkdf(txEncryptionIkm = X25519(txSenderPrivKey, receiverPubKey) || nonce, hkdfSalt) - // 4) encryptData(AES-CMAC-SIV, aesEncryptionKey, txSenderPubKey, plaintext, nonce) - // 5) output = nonce(32) || txSenderPubKey(32) || ciphertext - let mut tx_sender_privkey = [0u8; 32]; rand::thread_rng().fill_bytes(&mut tx_sender_privkey); let tx_sender_static = StaticSecret::from(tx_sender_privkey); @@ -216,7 +210,8 @@ fn offline_encrypt_secret_message( let aes_encryption_key = okm; // 32 bytes - let ciphertext = encrypt_data_cmac_siv(&aes_encryption_key, plaintext)?; + let associated_data = [&nonce[..], tx_sender_pubkey.as_bytes()]; + let ciphertext = encrypt_data_cmac_siv(&aes_encryption_key, plaintext, &associated_data)?; let mut out = Vec::with_capacity(32 + 32 + ciphertext.len()); out.extend_from_slice(&nonce); @@ -225,14 +220,16 @@ fn offline_encrypt_secret_message( Ok(out) } -fn encrypt_data_cmac_siv(aes_encryption_key: &[u8; 32], plaintext: &[u8]) -> Result> { +fn encrypt_data_cmac_siv( + aes_encryption_key: &[u8; 32], + plaintext: &[u8], + associated_data: &[&[u8]], +) -> Result> { let (cmac_key, ctr_key) = aes_encryption_key.split_at(16); let cmac_key: &[u8; 16] = cmac_key.try_into().expect("16 bytes"); let ctr_key: &[u8; 16] = ctr_key.try_into().expect("16 bytes"); - // Associated data items list contains a single empty slice. - let associated_data_items: Vec<&[u8]> = vec![&[]]; - let siv_tag = s2v_aes_cmac_siv(cmac_key, &associated_data_items, plaintext)?; + let siv_tag = s2v_aes_cmac_siv(cmac_key, associated_data, plaintext)?; // SIV => CTR IV with top bits cleared in last two 32-bit words. let mut ctr_iv = siv_tag; @@ -483,4 +480,27 @@ mod tests { .contains("invalid code_hash length") ); } + + #[test] + fn test_siv_associated_data_binding() { + let key = [0u8; 32]; + let plaintext = b"hello world"; + + let ad1 = [b"nonce1".as_slice(), b"pubkey1".as_slice()]; + let ad2 = [b"nonce2".as_slice(), b"pubkey1".as_slice()]; // Different nonce + let ad3 = [b"nonce1".as_slice(), b"pubkey2".as_slice()]; // Different pubkey + + let ct1 = encrypt_data_cmac_siv(&key, plaintext, &ad1).unwrap(); + let ct2 = encrypt_data_cmac_siv(&key, plaintext, &ad2).unwrap(); + let ct3 = encrypt_data_cmac_siv(&key, plaintext, &ad3).unwrap(); + + // Ciphertexts should be different because AD is different + assert_ne!(ct1, ct2, "Ciphertexts should differ with different nonce"); + assert_ne!(ct1, ct3, "Ciphertexts should differ with different pubkey"); + assert_ne!(ct2, ct3, "Ciphertexts should differ with both different"); + + // The first 16 bytes are the SIV tag; they must differ + assert_ne!(&ct1[..16], &ct2[..16], "SIV tags should differ with different nonce"); + assert_ne!(&ct1[..16], &ct3[..16], "SIV tags should differ with different pubkey"); + } } From 7c84f6b75507538a2eec735fb0a2e91c6d2ff4b3 Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Wed, 8 Apr 2026 16:53:01 +0700 Subject: [PATCH 6/9] run lint --- src/codec/cosmwasm_secret.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs index 784ddad..a3d9a2e 100644 --- a/src/codec/cosmwasm_secret.rs +++ b/src/codec/cosmwasm_secret.rs @@ -500,7 +500,15 @@ mod tests { assert_ne!(ct2, ct3, "Ciphertexts should differ with both different"); // The first 16 bytes are the SIV tag; they must differ - assert_ne!(&ct1[..16], &ct2[..16], "SIV tags should differ with different nonce"); - assert_ne!(&ct1[..16], &ct3[..16], "SIV tags should differ with different pubkey"); + assert_ne!( + &ct1[..16], + &ct2[..16], + "SIV tags should differ with different nonce" + ); + assert_ne!( + &ct1[..16], + &ct3[..16], + "SIV tags should differ with different pubkey" + ); } } From 4d49e994af7d9a13010dd24d96f1adc080bb5ce9 Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Thu, 16 Apr 2026 17:51:05 +0700 Subject: [PATCH 7/9] remove signer private_key --- src/codec/cosmwasm_secret.rs | 88 ++++++++++++++++++++++++------------ src/server/service.rs | 28 +++++++----- src/signer.rs | 5 -- src/signer/local.rs | 6 --- 4 files changed, 76 insertions(+), 51 deletions(-) diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs index a3d9a2e..4e658ee 100644 --- a/src/codec/cosmwasm_secret.rs +++ b/src/codec/cosmwasm_secret.rs @@ -2,7 +2,6 @@ use anyhow::{Context, Result, anyhow}; use cmac::{Cmac, Mac}; use cosmrs::{ AccountId, Any, - crypto::secp256k1, tx::{Body, Fee, SignDoc, SignerInfo}, }; use hkdf::Hkdf; @@ -35,12 +34,12 @@ pub struct Coin { pub amount: String, } -// Matches: secret.compute.v1beta1.MsgExecuteContract #[derive(Clone, PartialEq, ::prost::Message)] pub struct MsgExecuteContract { // sender sdk.AccAddress (20 bytes) #[prost(bytes, tag = "1")] pub sender: Vec, + // contract sdk.AccAddress (20 bytes) #[prost(bytes, tag = "2")] pub contract: Vec, // encrypted bytes (nonce||pubkey||ciphertext) @@ -110,24 +109,25 @@ pub fn encrypt_secret_execute_msg( .map_err(|_| anyhow!("pubkey should be 32 bytes"))?, ); - let code_hash_hex = code_hash.trim_start_matches("0x"); - let code_hash_bytes = hex::decode(code_hash_hex).context("invalid code_hash hex")?; - if code_hash_bytes.len() != 32 { + let code_hash_hex = code_hash.trim_start_matches("0x").to_lowercase(); + if code_hash_hex.len() != 64 { return Err(anyhow!( - "invalid code_hash length: expected 32 bytes (64 hex chars), got {}", - code_hash_bytes.len() + "invalid code_hash length: expected 64 hex chars, got {}", + code_hash_hex.len() )); } + if !code_hash_hex.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(anyhow!("invalid code_hash hex: contains non-hex characters")); + } - let mut plaintext = Vec::with_capacity(code_hash_bytes.len() + execute_msg_json.len()); - plaintext.extend_from_slice(&code_hash_bytes); + let mut plaintext = Vec::with_capacity(code_hash_hex.len() + execute_msg_json.len()); + plaintext.extend_from_slice(code_hash_hex.as_bytes()); plaintext.extend_from_slice(execute_msg_json); offline_encrypt_secret_message(&plaintext, &receiver_pubkey) } pub struct SignSecretTxParams { - pub signer_private_key: Vec, pub sender_address_bech32: String, pub contract_address_bech32: String, pub encrypted_execute_msg: Vec, @@ -139,7 +139,10 @@ pub struct SignSecretTxParams { pub memo: String, } -pub fn sign_secret_tx(params: SignSecretTxParams) -> Result> { +pub fn prepare_secret_tx_for_signing( + public_key: &[u8], + params: SignSecretTxParams, +) -> Result<(Vec, SignDoc)> { let sender_bytes = decode_acc_address_bech32(¶ms.sender_address_bech32)?; let contract_bytes = decode_acc_address_bech32(¶ms.contract_address_bech32)?; @@ -162,10 +165,12 @@ pub fn sign_secret_tx(params: SignSecretTxParams) -> Result> { let tx_body = Body::new(vec![msg_any], params.memo, 0u16); let fee_coin = parse_gas_prices_to_fee_coin(¶ms.gas_prices, params.gas_limit)?; - let signing_key = secp256k1::SigningKey::from_slice(¶ms.signer_private_key) - .map_err(|e| anyhow!("invalid signer private key: {e}"))?; - let signer_info = SignerInfo::single_direct(Some(signing_key.public_key()), params.sequence); + let verifying_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(public_key) + .map_err(|e| anyhow!("invalid signer public key: {e}"))?; + let pubkey = cosmrs::crypto::PublicKey::from(verifying_key); + + let signer_info = SignerInfo::single_direct(Some(pubkey), params.sequence); let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(fee_coin, params.gas_limit)); @@ -176,13 +181,31 @@ pub fn sign_secret_tx(params: SignSecretTxParams) -> Result> { let sign_doc = SignDoc::new(&tx_body, &auth_info, &chain_id, params.account_number) .map_err(|e| anyhow!("failed to create SignDoc: {e}"))?; - let tx_signed = sign_doc - .sign(&signing_key) - .map_err(|e| anyhow!("failed to sign SignDoc: {e}"))?; - tx_signed - .to_bytes() - .map_err(|e| anyhow!("failed to serialize signed tx: {e}")) + let sign_doc_bytes = sign_doc + .clone() + .into_bytes() + .map_err(|e| anyhow!("failed to serialize SignDoc: {e}"))?; + + Ok((sign_doc_bytes, sign_doc)) +} + +pub fn finalize_secret_tx(sign_doc: SignDoc, signature: &[u8]) -> Result> { + if signature.len() < 64 { + return Err(anyhow!( + "invalid signature length: expected at least 64 bytes, got {}", + signature.len() + )); + } + + let proto = sign_doc.into_proto(); + let tx_raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw { + body_bytes: proto.body_bytes, + auth_info_bytes: proto.auth_info_bytes, + signatures: vec![signature[..64].to_vec()], + }; + + Ok(tx_raw.encode_to_vec()) } fn offline_encrypt_secret_message( @@ -209,9 +232,13 @@ fn offline_encrypt_secret_message( .map_err(|e| anyhow!("HKDF expand failed: {e}"))?; let aes_encryption_key = okm; // 32 bytes + // The Go implementation calls: cipher.Seal(nil, plaintext, []byte{}) + // This passes ONE variadic arg (an empty slice) as associated data. + // S2V with [empty] != S2V with [] — they produce different SIV tags. + let empty: &[u8] = &[]; + let associated_data: &[&[u8]] = &[empty]; - let associated_data = [&nonce[..], tx_sender_pubkey.as_bytes()]; - let ciphertext = encrypt_data_cmac_siv(&aes_encryption_key, plaintext, &associated_data)?; + let ciphertext = encrypt_data_cmac_siv(&aes_encryption_key, plaintext, associated_data)?; let mut out = Vec::with_capacity(32 + 32 + ciphertext.len()); out.extend_from_slice(&nonce); @@ -460,24 +487,29 @@ mod tests { result.err() ); - // Should fail with invalid hex (odd number of characters) - let result = encrypt_secret_execute_msg("invalidhex", pubkey_hex, msg); + // Should fail with wrong length (too short) + let result = encrypt_secret_execute_msg("deadbeef", pubkey_hex, msg); assert!(result.is_err()); assert!( result .unwrap_err() .to_string() - .contains("invalid code_hash hex") + .contains("invalid code_hash length"), + "expected length error" ); - // Should fail with invalid length (even characters, but not 32 bytes) - let result = encrypt_secret_execute_msg("deadbeef", pubkey_hex, msg); + // Should fail with correct length (64 chars) but non-hex characters + // 63 valid hex chars + 'g' (non-hex) + let non_hex_hash = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1g"; + assert_eq!(non_hex_hash.len(), 64); + let result = encrypt_secret_execute_msg(non_hex_hash, pubkey_hex, msg); assert!(result.is_err()); assert!( result .unwrap_err() .to_string() - .contains("invalid code_hash length") + .contains("invalid code_hash hex"), + "expected hex char error" ); } diff --git a/src/server/service.rs b/src/server/service.rs index 7d65669..a92e1bc 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -1,5 +1,6 @@ use crate::codec::cosmwasm_secret::{ - SignSecretTxParams, encrypt_secret_execute_msg, secret_execute_msg_json, sign_secret_tx, + SignSecretTxParams, encrypt_secret_execute_msg, finalize_secret_tx, + prepare_secret_tx_for_signing, secret_execute_msg_json, }; use crate::codec::evm::decode_tx; use crate::codec::flow; @@ -21,7 +22,7 @@ use crate::proto::fkms::v1::{ }; use crate::server::Server; use crate::server::utils::filter_usd_signal; -use k256::sha2::Sha512; +use k256::sha2::{Sha256, Sha512}; use sha3::{Digest, Sha3_256}; use std::collections::HashMap; use tonic::{Request, Response, Status}; @@ -535,15 +536,7 @@ impl FkmsService for Server { ) .map_err(|e| Status::internal(format!("Failed to encrypt execute msg: {e}")))?; - let pk_bytes = signer.private_key().ok_or_else(|| { - Status::failed_precondition(format!( - "Secret signer is missing a private key for sender {}", - signer_payload.sender - )) - })?; - let secret_params = SignSecretTxParams { - signer_private_key: pk_bytes.to_vec(), sender_address_bech32: signer_payload.sender.clone(), contract_address_bech32: signer_payload.contract_address.clone(), encrypted_execute_msg, @@ -555,8 +548,19 @@ impl FkmsService for Server { memo: signer_payload.memo.clone(), }; - let tx_blob = sign_secret_tx(secret_params) - .map_err(|e| Status::internal(format!("Failed to sign Secret tx: {e}")))?; + let (sign_doc_bytes, sign_doc) = + prepare_secret_tx_for_signing(signer.public_key(), secret_params).map_err( + |e| Status::internal(format!("Failed to prepare Secret tx: {e}")), + )?; + + let digest = Sha256::digest(&sign_doc_bytes); + let signature = signer.sign(&digest).await.map_err(|e| { + error!("failed to sign secret message: {:?}", e); + Status::internal(format!("Failed to sign message: {e}")) + })?; + + let tx_blob = finalize_secret_tx(sign_doc, &signature) + .map_err(|e| Status::internal(format!("Failed to finalize Secret tx: {e}")))?; info!("successfully signed secret cosmwasm message"); Ok(Response::new(SignSecretResponse { tx_blob })) diff --git a/src/signer.rs b/src/signer.rs index c36bd0b..7ab821d 100644 --- a/src/signer.rs +++ b/src/signer.rs @@ -24,11 +24,6 @@ pub trait Signer: Send + Sync + 'static { fn address(&self) -> &str; fn chain_type(&self) -> &ChainType; - - // For remote signers (AWS/HSM), this is left as `None`. - fn private_key(&self) -> Option<&[u8]> { - None - } } pub fn public_key_to_evm_address(public_key: &[u8]) -> anyhow::Result { diff --git a/src/signer/local.rs b/src/signer/local.rs index 7e78130..73b2178 100644 --- a/src/signer/local.rs +++ b/src/signer/local.rs @@ -25,7 +25,6 @@ use stellar_strkey::Strkey; pub struct LocalSigner { signing_key: SigningKey, public_key: Vec, - private_key: Vec, address: String, chain_type: ChainType, } @@ -109,7 +108,6 @@ impl LocalSigner { Ok(LocalSigner { signing_key, public_key, - private_key: private_key.to_vec(), address, chain_type: chain_type.clone(), }) @@ -181,10 +179,6 @@ impl Signer for LocalSigner { fn chain_type(&self) -> &ChainType { &self.chain_type } - - fn private_key(&self) -> Option<&[u8]> { - Some(&self.private_key) - } } fn create_ecdsa_public_key(signing_key: &EcdsaSigningKey, compressed: bool) -> Vec From 9730fdc1347416cb4b0cb66d9ef71c7228fbf466 Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Thu, 16 Apr 2026 18:05:21 +0700 Subject: [PATCH 8/9] run lint --- src/codec/cosmwasm_secret.rs | 4 +++- src/server/service.rs | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/codec/cosmwasm_secret.rs b/src/codec/cosmwasm_secret.rs index 4e658ee..f25b551 100644 --- a/src/codec/cosmwasm_secret.rs +++ b/src/codec/cosmwasm_secret.rs @@ -117,7 +117,9 @@ pub fn encrypt_secret_execute_msg( )); } if !code_hash_hex.chars().all(|c| c.is_ascii_hexdigit()) { - return Err(anyhow!("invalid code_hash hex: contains non-hex characters")); + return Err(anyhow!( + "invalid code_hash hex: contains non-hex characters" + )); } let mut plaintext = Vec::with_capacity(code_hash_hex.len() + execute_msg_json.len()); diff --git a/src/server/service.rs b/src/server/service.rs index a92e1bc..551dc4f 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -512,13 +512,11 @@ impl FkmsService for Server { .get(&(ChainType::Secret, signer_payload.sender.clone())) { Some(signer) => { - let signals: Vec<(String, u64)> = tunnel_packet + let (symbols, rates): (Vec, Vec) = tunnel_packet .signals .iter() .filter_map(filter_usd_signal) - .collect(); - let symbols: Vec = signals.iter().map(|(sym, _)| sym.clone()).collect(); - let rates: Vec = signals.iter().map(|(_, rate)| *rate).collect(); + .unzip(); let resolve_time = u64::try_from(tunnel_packet.timestamp) .map_err(|_| Status::invalid_argument("Timestamp must be non-negative"))?; From 7ce7c5f8d0469941518b79b2f981b26564565146 Mon Sep 17 00:00:00 2001 From: Nattapat Iammelap Date: Thu, 16 Apr 2026 18:09:32 +0700 Subject: [PATCH 9/9] rename proto field --- proto/fkms/v1/signer.proto | 2 +- src/server/service.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proto/fkms/v1/signer.proto b/proto/fkms/v1/signer.proto index e8228fc..ff729eb 100644 --- a/proto/fkms/v1/signer.proto +++ b/proto/fkms/v1/signer.proto @@ -115,7 +115,7 @@ message SecretSignerPayload { string gas_prices = 7; string memo = 8; string code_hash = 9; - string pubkey = 10; + string chain_pubkey = 10; } message Tss { diff --git a/src/server/service.rs b/src/server/service.rs index 551dc4f..4317403 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -529,7 +529,7 @@ impl FkmsService for Server { let encrypted_execute_msg = encrypt_secret_execute_msg( &signer_payload.code_hash, - &signer_payload.pubkey, + &signer_payload.chain_pubkey, &execute_msg_json, ) .map_err(|e| Status::internal(format!("Failed to encrypt execute msg: {e}")))?;