Skip to content

feat(chain): unify key safety and expand tx primitives beyond Cascade#2

Open
mateeullahmalik wants to merge 4 commits intomainfrom
feat/chain-key-safety-tx-primitives
Open

feat(chain): unify key safety and expand tx primitives beyond Cascade#2
mateeullahmalik wants to merge 4 commits intomainfrom
feat/chain-key-safety-tx-primitives

Conversation

@mateeullahmalik
Copy link
Contributor

Summary

This PR delivers the next SDK-RS step requested after Cascade-first support:

  1. clean/safe key handling for both tx-signing and message-signing,
  2. stronger chain interaction primitives (account/fee/tx/send/confirm), plus simulation-backed gas path.

Why

The Rust SDK was already good for Cascade orchestration, but chain interaction needed to be elevated toward sdk-go baseline patterns and safety discipline.

What changed

1) Key handling: one clear/safe path

  • Added src/keys.rs with unified identity derivation:
    • SigningIdentity::from_mnemonic(...)
    • returns both:
      • chain tx signing key (cosmrs::crypto::secp256k1::SigningKey)
      • arbitrary message signing key (k256::ecdsa::SigningKey)
  • Added explicit validation helpers:
    • validate_address(...)
    • validate_chain_prefix(...)
  • Added reusable helper:
    • derive_signing_keys_from_mnemonic(...)
  • Refactored examples (golden_devnet, ui_server) to consume centralized key derivation (removed duplicated derivation blocks).

2) Chain interaction: beyond Cascade

Account + fee + tx confirmation

  • get_account_info(address)
  • calculate_fee_amount(gas_limit) (derived from configured gas price)
  • get_tx(tx_hash)
  • wait_for_tx_confirmation(tx_hash, timeout_secs)

Generic tx primitives

  • build_signed_tx(...)
  • broadcast_signed_tx(...)
  • send_any_msgs(...)
  • broadcast mode support via BroadcastMode::{Async, Sync, Commit}

Simulation-backed gas path

  • simulate_gas_for_tx(...)
  • build_signed_tx_with_simulation(...) (fallback + gas-adjustment)

Common Lumera wrapper

  • request_action_tx(...) implemented on top of generic tx path and simulation-backed gas.

3) Richer event extraction helpers

  • Added reusable event extraction from tx logs/response:
    • extract_event_attribute_from_log(...)
    • extract_event_attributes_from_tx_response(...)
    • get_tx_event_attributes(tx_hash)
    • wait_for_event_attribute(tx_hash, event_type, attr_key, timeout_secs)
  • Existing extract_action_id_from_log(...) now delegates to generic extraction helper.

4) Docs

  • Updated README.md chain capabilities section.
  • Added feasibility/gap document:
    • docs/chain-feasibility-gap-analysis.md

Safety / correctness checks added

  • Early signer/creator mismatch check before tx submission.
  • Prefix/address validation helpers for identity sanity.
  • Safer tx flow composition around explicit build/sign/broadcast boundaries.

Validation

  • cargo test --lib passes.
  • Current result: 17 tests passing.

Notes

This PR intentionally keeps diffs focused and incremental while opening a clean base for additional Lumera msg wrappers and deeper tx result helpers.

@roomote-v0
Copy link

roomote-v0 bot commented Mar 25, 2026

Rooviewer Clock   See task

All 4 previously flagged issues have been addressed. The latest commit (10f979d) only bumps rustls-webpki 0.103.9 -> 0.103.10 in Cargo.lock to resolve RUSTSEC-2026-0049. No new issues found.

  • validate_signer_matches_creator in src/chain.rs uses split_once('1') instead of rsplit_once('1') for bech32 HRP extraction -- incorrect per bech32 spec
  • validate_chain_prefix in src/keys.rs has the same split_once('1') vs rsplit_once('1') issue
  • request_action_tx in src/chain.rs lacks the sequence-mismatch retry logic that register_action has
  • examples/ui_server.rs retains unused imports (bip32, bip39, FromStr) after the refactor to derive_signing_keys_from_mnemonic
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

src/chain.rs Outdated
signing_key: &cosmrs::crypto::secp256k1::SigningKey,
creator: &str,
) -> Result<(), SdkError> {
let (hrp, _) = creator.split_once('1').ok_or_else(|| {
Copy link

Choose a reason for hiding this comment

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

split_once('1') splits at the first 1 in the address string. Per the bech32 spec the human-readable part (HRP) is everything before the last 1 separator. If the HRP ever contains the digit 1 (valid in bech32), this will extract the wrong prefix and silently produce an incorrect derived address for comparison. With the current lumera prefix this is harmless, but rsplit_once('1') would be correct for any bech32 address.

Suggested change
let (hrp, _) = creator.split_once('1').ok_or_else(|| {
let (hrp, _) = creator.rsplit_once('1').ok_or_else(|| {

Fix it with Roo Code or mention @roomote and request a fix.

src/keys.rs Outdated
}

pub fn validate_chain_prefix(expected_address: &str, expected_hrp: &str) -> Result<(), SdkError> {
let (actual_hrp, _) = expected_address.split_once('1').ok_or_else(|| {
Copy link

Choose a reason for hiding this comment

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

Same split_once('1') issue as in validate_signer_matches_creator: bech32 defines the HRP as everything before the last 1 separator, not the first. Should be rsplit_once('1') to handle HRPs that contain the digit 1.

Suggested change
let (actual_hrp, _) = expected_address.split_once('1').ok_or_else(|| {
let (actual_hrp, _) = expected_address.rsplit_once('1').ok_or_else(|| {

Fix it with Roo Code or mention @roomote and request a fix.

Comment on lines +481 to +546
pub async fn request_action_tx(
&self,
signing_key: &cosmrs::crypto::secp256k1::SigningKey,
tx: RequestActionTxInput,
memo: impl Into<String>,
) -> Result<RequestActionSubmitResult, SdkError> {
self.validate_signer_matches_creator(signing_key, &tx.creator)?;

let mut msg_bytes = Vec::new();
MsgRequestActionProto {
creator: tx.creator.clone(),
action_type: tx.action_type,
metadata: tx.metadata,
price: tx.price,
expiration_time: tx.expiration_time,
file_size_kbs: tx.file_size_kbs,
app_pubkey: tx.app_pubkey,
}
.encode(&mut msg_bytes)
.map_err(|e| SdkError::Serialization(e.to_string()))?;

let any = Any {
type_url: "/lumera.action.v1.MsgRequestAction".to_string(),
value: msg_bytes,
};

let (tx_raw, _gas) = self
.build_signed_tx_with_simulation(
signing_key,
&tx.creator,
vec![any],
memo,
500_000,
1.3,
)
.await?;

let broadcast = self
.broadcast_signed_tx(&tx_raw, BroadcastMode::Commit)
.await?;

if broadcast.check_tx_code.unwrap_or_default() != 0 {
return Err(SdkError::Chain(format!(
"check_tx failed: {}",
broadcast.log
)));
}
if broadcast.deliver_tx_code.unwrap_or_default() != 0 {
return Err(SdkError::Chain(format!(
"deliver_tx failed: {}",
broadcast.log
)));
}

let action_id = extract_action_id_from_log(&broadcast.log).ok_or_else(|| {
SdkError::Chain(format!(
"unable to extract action_id from commit log: {}",
broadcast.log
))
})?;

Ok(RequestActionSubmitResult {
tx_hash: broadcast.tx_hash,
action_id,
})
}
Copy link

Choose a reason for hiding this comment

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

request_action_tx submits the same MsgRequestAction as register_action, but unlike register_action it has no sequence-mismatch retry loop. If the on-chain sequence drifts (e.g. concurrent tx from the same account, or a stale REST cache), this will fail on the first attempt with a sequence error. The existing register_action handles this with a 3-attempt retry that re-fetches the account. Consider adding similar retry logic here, or extracting the retry loop into a shared helper that both methods can use.

Fix it with Roo Code or mention @roomote and request a fix.

Comment on lines 134 to 138
) -> Result<(cosmrs::crypto::secp256k1::SigningKey, K256SigningKey), ApiError> {
let m = Mnemonic::parse(mnemonic).map_err(|e| ApiError {
error: format!("invalid LUMERA_MNEMONIC: {e}"),
})?;
let seed = m.to_seed("");
let path = DerivationPath::from_str("m/44'/118'/0'/0/0").map_err(|e| ApiError {
error: format!("invalid derivation path: {e}"),
})?;
let xprv = XPrv::derive_from_path(seed, &path).map_err(|e| ApiError {
error: format!("failed deriving key from mnemonic: {e}"),
})?;
let sk_bytes = xprv.private_key().to_bytes();
let chain_sk =
cosmrs::crypto::secp256k1::SigningKey::from_slice(&sk_bytes).map_err(|e| ApiError {
error: format!("failed creating chain signing key: {e}"),
})?;
let arb_sk = K256SigningKey::from_slice(&sk_bytes).map_err(|e| ApiError {
error: format!("failed creating arbitrary signing key: {e}"),
})?;
Ok((chain_sk, arb_sk))
lumera_sdk_rs::keys::derive_signing_keys_from_mnemonic(mnemonic).map_err(|e| ApiError {
error: e.to_string(),
})
}
Copy link

Choose a reason for hiding this comment

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

The function body now delegates to derive_signing_keys_from_mnemonic, but the file still imports bip32::{DerivationPath, XPrv}, bip39::Mnemonic, and std::str::FromStr (lines 4, 17-18) which were only used by the old inline derivation logic. These are now dead imports and will trigger compiler warnings (or errors under #[deny(unused_imports)]).

Fix it with Roo Code or mention @roomote and request a fix.

@mateeullahmalik
Copy link
Contributor Author

Addressed all review comments and CI red checks.

Changes pushed:

  • bech32 HRP extraction fixed to rsplit_once('1') in both key/prefix validation paths.
  • request_action_tx now includes sequence-mismatch retry loop (3 attempts), aligned with existing register flow behavior.
  • removed dead imports in examples/ui_server.rs and examples/golden_devnet.rs.
  • cargo-audit failure fixed by updating rustls-webpki from 0.103.9 to 0.103.10 in Cargo.lock (RUSTSEC-2026-0049 mitigation).

Validation done locally:

  • cargo fmt -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test

Latest commit: 10f979d.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant