From 7265b3a6556e6d2175e152d31aea84ecf6a1fda7 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Mon, 27 Jan 2025 18:55:44 -0300 Subject: [PATCH 01/11] feat: decode tx on sign_tx for substrate --- packages/kos/src/chains/substrate/mod.rs | 45 +++++++-- packages/kos/src/chains/substrate/models.rs | 106 +++++++++++++++++++- 2 files changed, 140 insertions(+), 11 deletions(-) diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index 93434eb..d589370 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -1,15 +1,16 @@ mod models; +use crate::chains::substrate::models::ExtrinsicPayload; use crate::chains::util::private_key_from_vec; use crate::chains::{Chain, ChainError, Transaction, TxInfo}; -use crate::crypto::hash::blake2b_64_digest; +use crate::crypto::hash::{blake2b_64_digest, blake2b_digest}; use crate::crypto::sr25519::Sr25519Trait; use crate::crypto::{b58, bip32, sr25519}; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use models::{Call, CallArgs}; -use parity_scale_codec::Decode; +use parity_scale_codec::{Decode, Encode}; const LOWER_MASK: u16 = 0x3FFF; const TYPE1_ACCOUNT_ID: u16 = 63; @@ -108,8 +109,25 @@ impl Chain for Substrate { private_key: Vec, mut tx: Transaction, ) -> Result { - let sig = self.sign_raw(private_key, tx.raw_data.clone())?; - tx.signature = [[1u8].to_vec(), sig].concat(); + let extrinsic = ExtrinsicPayload::from_raw(tx.raw_data.clone())?; + + let signature = { + let full_unsigned_payload_scale_bytes = extrinsic.to_bytes(); + + // If payload is longer than 256 bytes, we hash it and sign the hash instead: + if full_unsigned_payload_scale_bytes.len() > 256 { + self.sign_raw( + private_key, + blake2b_digest(&full_unsigned_payload_scale_bytes)?, + )? + } else { + self.sign_raw(private_key, full_unsigned_payload_scale_bytes)? + } + }; + + // tx.raw_data = extrinsic.encode_signed(tx.raw_data, sig.clone()); + + tx.signature = [[1u8].to_vec(), signature].concat(); Ok(tx) } @@ -142,8 +160,8 @@ impl Chain for Substrate { #[cfg(test)] mod test { - - use crate::chains::Chain; + use crate::chains::{Chain, Transaction}; + use crate::crypto::base64::simple_base64_decode; use alloc::string::{String, ToString}; #[test] @@ -191,10 +209,21 @@ mod test { let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); - let path = String::from(""); + let path = dot.get_path(0, false); let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); - let _pvk = dot.derive(seed, path).unwrap(); + let pvk = dot.derive(seed, path).unwrap(); + + let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHCRAXUCSAAA/E0PABoAAACRsXG7FY4tOEj6I6nxwlGC+44gMTssHrSSGdp6cM6Qw9QXMtYkIUOthrBb+DGJV708aGKwcFqtBLEzg3sSYncgAA==").unwrap(); + + let tx = Transaction { + raw_data, + signature: Vec::new(), + tx_hash: Vec::new(), + options: None, + }; + + let signed_tx = dot.sign_tx(pvk, tx).unwrap(); } #[test] diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index 947a0ae..922178e 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -1,8 +1,9 @@ +use crate::chains::ChainError; +use crate::crypto::bignum::U256; +use aes_gcm::aead::Buffer; use alloc::vec; use alloc::vec::Vec; -use parity_scale_codec::{Decode, Input}; - -use crate::crypto::bignum::U256; +use parity_scale_codec::{Compact, Decode, Encode, Input}; #[derive(Decode)] pub struct Call { @@ -136,3 +137,102 @@ impl Decode for UIntCompact { } } } + +#[derive(Debug)] +pub struct ExtrinsicPayload { + pub call_index: [u8; 2], + pub destination: [u8; 32], + pub value: [u8; 2], + pub era: [u8; 1], + pub nonce: u32, + pub tip: u8, + pub mode: u8, + pub spec_version: u32, + pub transaction_version: u32, + pub genesis_hash: [u8; 32], + pub block_hash: [u8; 32], + pub metadata_hash: u8, +} + +impl ExtrinsicPayload { + pub fn from_raw(bytes: Vec) -> Result { + let mut input = bytes.as_slice(); + + let mut call_index = [0u8; 2]; + call_index.copy_from_slice(&input[0..2]); + input = &input[2..]; + + let mut destination = [0u8; 32]; + destination.copy_from_slice(&input[0..32]); + input = &input[32..]; + + let mut value = [0u8; 2]; + value.copy_from_slice(&input[0..2]); + input = &input[2..]; + + let mut era = [0u8; 1]; + era.copy_from_slice(&input[0..1]); + input = &input[1..]; + + let nonce = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); + input = &input[4..]; + + let tip = input[0]; + input = &input[1..]; + + let mode = input[0]; + input = &input[1..]; + + let spec_version = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); + input = &input[4..]; + + let transaction_version = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); + input = &input[4..]; + + let mut genesis_hash = [0u8; 32]; + genesis_hash.copy_from_slice(&input[0..32]); + input = &input[32..]; + + let mut block_hash = [0u8; 32]; + + block_hash.copy_from_slice(&input[0..32]); + input = &input[32..]; + + let metadata_hash = if input.len() > 0 { input[0] } else { 0 }; + + Ok(ExtrinsicPayload { + call_index, + destination, + value, + era, + nonce, + tip, + mode, + spec_version, + transaction_version, + genesis_hash, + block_hash, + metadata_hash, + }) + } + + pub fn to_bytes(&self) -> Vec { + let mut encoded = Vec::new(); + + encoded.extend_from_slice(&self.call_index); + encoded.push(0x00); + encoded.extend_from_slice(&self.destination); + encoded.extend_from_slice(&self.value); + encoded.extend_from_slice(&self.era); + encoded.extend_from_slice(Compact(self.nonce).encode().as_slice()); + encoded.extend_from_slice(Compact(self.tip).encode().as_slice()); + encoded.push(self.mode); + encoded.extend_from_slice(&self.spec_version.encode()); + encoded.extend_from_slice(&self.transaction_version.encode()); + encoded.extend_from_slice(&self.genesis_hash); + encoded.extend_from_slice(&self.block_hash); + encoded.push(self.metadata_hash); + + encoded + } +} From eb3d3813477460f9afe24e57fa21867c338f0da2 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Mon, 27 Jan 2025 23:05:52 -0300 Subject: [PATCH 02/11] feat: prepare signed tx for broadcast --- packages/kos/src/chains/substrate/mod.rs | 12 ++-- packages/kos/src/chains/substrate/models.rs | 67 +++++++++++++++------ 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index d589370..428c6f5 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -10,7 +10,7 @@ use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use models::{Call, CallArgs}; -use parity_scale_codec::{Decode, Encode}; +use parity_scale_codec::Decode; const LOWER_MASK: u16 = 0x3FFF; const TYPE1_ACCOUNT_ID: u16 = 63; @@ -117,15 +117,17 @@ impl Chain for Substrate { // If payload is longer than 256 bytes, we hash it and sign the hash instead: if full_unsigned_payload_scale_bytes.len() > 256 { self.sign_raw( - private_key, - blake2b_digest(&full_unsigned_payload_scale_bytes)?, + private_key.clone(), + blake2b_digest(&full_unsigned_payload_scale_bytes).to_vec(), )? } else { - self.sign_raw(private_key, full_unsigned_payload_scale_bytes)? + self.sign_raw(private_key.clone(), full_unsigned_payload_scale_bytes)? } }; - // tx.raw_data = extrinsic.encode_signed(tx.raw_data, sig.clone()); + let public_key: [u8; 32] = self.get_pbk(private_key)?.try_into().unwrap(); + + tx.raw_data = extrinsic.encode_with_signature(&public_key, &signature); tx.signature = [[1u8].to_vec(), signature].concat(); Ok(tx) diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index 922178e..c9acc9d 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -1,6 +1,5 @@ use crate::chains::ChainError; use crate::crypto::bignum::U256; -use aes_gcm::aead::Buffer; use alloc::vec; use alloc::vec::Vec; use parity_scale_codec::{Compact, Decode, Encode, Input}; @@ -143,15 +142,15 @@ pub struct ExtrinsicPayload { pub call_index: [u8; 2], pub destination: [u8; 32], pub value: [u8; 2], - pub era: [u8; 1], - pub nonce: u32, + pub era: [u8; 2], + pub nonce: [u8; 1], pub tip: u8, pub mode: u8, pub spec_version: u32, pub transaction_version: u32, pub genesis_hash: [u8; 32], pub block_hash: [u8; 32], - pub metadata_hash: u8, + pub _metadata_hash: u8, } impl ExtrinsicPayload { @@ -163,19 +162,20 @@ impl ExtrinsicPayload { input = &input[2..]; let mut destination = [0u8; 32]; - destination.copy_from_slice(&input[0..32]); - input = &input[32..]; + destination.copy_from_slice(&input[1..33]); + input = &input[33..]; let mut value = [0u8; 2]; value.copy_from_slice(&input[0..2]); input = &input[2..]; - let mut era = [0u8; 1]; - era.copy_from_slice(&input[0..1]); - input = &input[1..]; + let mut era = [0u8; 2]; + era.copy_from_slice(&input[0..2]); + input = &input[2..]; - let nonce = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); - input = &input[4..]; + let mut nonce = [0u8; 1]; + nonce.copy_from_slice(&input[0..1]); + input = &input[1..]; let tip = input[0]; input = &input[1..]; @@ -198,7 +198,7 @@ impl ExtrinsicPayload { block_hash.copy_from_slice(&input[0..32]); input = &input[32..]; - let metadata_hash = if input.len() > 0 { input[0] } else { 0 }; + let _metadata_hash = if !input.is_empty() { input[0] } else { 0 }; Ok(ExtrinsicPayload { call_index, @@ -212,7 +212,7 @@ impl ExtrinsicPayload { transaction_version, genesis_hash, block_hash, - metadata_hash, + _metadata_hash, }) } @@ -220,19 +220,52 @@ impl ExtrinsicPayload { let mut encoded = Vec::new(); encoded.extend_from_slice(&self.call_index); - encoded.push(0x00); encoded.extend_from_slice(&self.destination); encoded.extend_from_slice(&self.value); encoded.extend_from_slice(&self.era); - encoded.extend_from_slice(Compact(self.nonce).encode().as_slice()); - encoded.extend_from_slice(Compact(self.tip).encode().as_slice()); + encoded.extend_from_slice(self.nonce.encode().as_slice()); + encoded.extend_from_slice(self.tip.encode().as_slice()); encoded.push(self.mode); encoded.extend_from_slice(&self.spec_version.encode()); encoded.extend_from_slice(&self.transaction_version.encode()); encoded.extend_from_slice(&self.genesis_hash); encoded.extend_from_slice(&self.block_hash); - encoded.push(self.metadata_hash); + // encoded.push(self.metadata_hash); encoded } + + pub fn encode_with_signature(&self, public_key: &[u8; 32], signature: &[u8]) -> Vec { + let mut encoded = Vec::new(); + + let signed_flag: u8 = 0b1000_0000; + let transaction_version = 4; + encoded.push(signed_flag | transaction_version); + + encoded.push(0x00); + encoded.extend_from_slice(public_key); + + encoded.push(0x01); + + encoded.extend_from_slice(signature); + + encoded.extend_from_slice(&self.era); + encoded.extend_from_slice(self.nonce.as_slice()); + encoded.push(self.tip); + encoded.push(self.mode); + + encoded.extend_from_slice(&self.call_index); + + encoded.push(0x00); + + encoded.extend_from_slice(&self.destination); + encoded.extend_from_slice(&self.value); + + let length = Compact(encoded.len() as u32).encode(); + let mut complete_encoded = Vec::with_capacity(length.len() + encoded.len()); + complete_encoded.extend_from_slice(&length); + complete_encoded.extend_from_slice(&encoded); + + complete_encoded + } } From 0c40d1c5fd85b6bfa03ac5a773d758072a3992f0 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Tue, 28 Jan 2025 11:07:34 -0300 Subject: [PATCH 03/11] fix: substrate tx payload for signing --- packages/kos/src/chains/substrate/mod.rs | 7 +++++-- packages/kos/src/chains/substrate/models.rs | 13 ++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index 428c6f5..fd8cc0d 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -112,7 +112,7 @@ impl Chain for Substrate { let extrinsic = ExtrinsicPayload::from_raw(tx.raw_data.clone())?; let signature = { - let full_unsigned_payload_scale_bytes = extrinsic.to_bytes(); + let full_unsigned_payload_scale_bytes = tx.raw_data.clone(); // If payload is longer than 256 bytes, we hash it and sign the hash instead: if full_unsigned_payload_scale_bytes.len() > 256 { @@ -216,7 +216,7 @@ mod test { let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); let pvk = dot.derive(seed, path).unwrap(); - let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHCRAXUCSAAA/E0PABoAAACRsXG7FY4tOEj6I6nxwlGC+44gMTssHrSSGdp6cM6Qw9QXMtYkIUOthrBb+DGJV708aGKwcFqtBLEzg3sSYncgAA==").unwrap(); + let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHCRAXUDYAAA/E0PABoAAACRsXG7FY4tOEj6I6nxwlGC+44gMTssHrSSGdp6cM6Qw36KXLq5dgOoEvZRpzirvfO3HDN6fM3bEwtF1XTUSlrGAA==").unwrap(); let tx = Transaction { raw_data, @@ -226,6 +226,9 @@ mod test { }; let signed_tx = dot.sign_tx(pvk, tx).unwrap(); + + assert_eq!(signed_tx.signature.len(), 65); + assert_eq!(signed_tx.raw_data.len(), 143); } #[test] diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index c9acc9d..132f564 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -150,7 +150,7 @@ pub struct ExtrinsicPayload { pub transaction_version: u32, pub genesis_hash: [u8; 32], pub block_hash: [u8; 32], - pub _metadata_hash: u8, + pub metadata_hash: u8, } impl ExtrinsicPayload { @@ -198,7 +198,7 @@ impl ExtrinsicPayload { block_hash.copy_from_slice(&input[0..32]); input = &input[32..]; - let _metadata_hash = if !input.is_empty() { input[0] } else { 0 }; + let metadata_hash = if !input.is_empty() { input[0] } else { 0 }; Ok(ExtrinsicPayload { call_index, @@ -212,14 +212,15 @@ impl ExtrinsicPayload { transaction_version, genesis_hash, block_hash, - _metadata_hash, + metadata_hash, }) } + #[allow(dead_code)] pub fn to_bytes(&self) -> Vec { let mut encoded = Vec::new(); - encoded.extend_from_slice(&self.call_index); + encoded.push(0x00); encoded.extend_from_slice(&self.destination); encoded.extend_from_slice(&self.value); encoded.extend_from_slice(&self.era); @@ -230,8 +231,10 @@ impl ExtrinsicPayload { encoded.extend_from_slice(&self.transaction_version.encode()); encoded.extend_from_slice(&self.genesis_hash); encoded.extend_from_slice(&self.block_hash); - // encoded.push(self.metadata_hash); + if self.metadata_hash != 0 { + encoded.push(self.metadata_hash); + } encoded } From 23086440a4db2eb847cdf1dd28c894b6c56fc261 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Tue, 28 Jan 2025 11:10:41 -0300 Subject: [PATCH 04/11] fix: enhanced public key check on tx encoding --- packages/kos/src/chains/substrate/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index fd8cc0d..db1f32d 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -125,7 +125,10 @@ impl Chain for Substrate { } }; - let public_key: [u8; 32] = self.get_pbk(private_key)?.try_into().unwrap(); + let pbk_vec = self.get_pbk(private_key)?; + let public_key: [u8; 32] = pbk_vec + .try_into() + .map_err(|_| ChainError::InvalidPublicKey)?; tx.raw_data = extrinsic.encode_with_signature(&public_key, &signature); From 4c17597b1ca0afbc798654a56ca8ee624fb3d40a Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Tue, 28 Jan 2025 11:28:22 -0300 Subject: [PATCH 05/11] chore: document the encoding format and extract magic numbers --- packages/kos/src/chains/substrate/models.rs | 45 ++++++++------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index 132f564..b17b825 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -4,6 +4,11 @@ use alloc::vec; use alloc::vec::Vec; use parity_scale_codec::{Compact, Decode, Encode, Input}; +const SIGNED_FLAG: u8 = 0b1000_0000; +const TRANSACTION_VERSION: u8 = 4; +const PUBLIC_KEY_TYPE: u8 = 0x00; +const SIGNATURE_TYPE: u8 = 0x01; + #[derive(Decode)] pub struct Call { pub _call_index: CallIndex, @@ -137,9 +142,13 @@ impl Decode for UIntCompact { } } -#[derive(Debug)] +/// Represents the payload of a Substrate extrinsic (transaction) that will be signed. +/// This structure contains all the necessary fields required for transaction signing. +#[allow(dead_code)] pub struct ExtrinsicPayload { + /// The call index identifying the function being called (module index + function index) pub call_index: [u8; 2], + /// The destination account's public key or address pub destination: [u8; 32], pub value: [u8; 2], pub era: [u8; 2], @@ -216,39 +225,17 @@ impl ExtrinsicPayload { }) } - #[allow(dead_code)] - pub fn to_bytes(&self) -> Vec { - let mut encoded = Vec::new(); - encoded.extend_from_slice(&self.call_index); - encoded.push(0x00); - encoded.extend_from_slice(&self.destination); - encoded.extend_from_slice(&self.value); - encoded.extend_from_slice(&self.era); - encoded.extend_from_slice(self.nonce.encode().as_slice()); - encoded.extend_from_slice(self.tip.encode().as_slice()); - encoded.push(self.mode); - encoded.extend_from_slice(&self.spec_version.encode()); - encoded.extend_from_slice(&self.transaction_version.encode()); - encoded.extend_from_slice(&self.genesis_hash); - encoded.extend_from_slice(&self.block_hash); - - if self.metadata_hash != 0 { - encoded.push(self.metadata_hash); - } - encoded - } - + /// Encodes the payload with a signature using the Substrate transaction format. + /// The format is: length + (version + signature + era + nonce + tip + call + params) pub fn encode_with_signature(&self, public_key: &[u8; 32], signature: &[u8]) -> Vec { let mut encoded = Vec::new(); - let signed_flag: u8 = 0b1000_0000; - let transaction_version = 4; - encoded.push(signed_flag | transaction_version); + encoded.push(SIGNED_FLAG | TRANSACTION_VERSION); - encoded.push(0x00); + encoded.push(PUBLIC_KEY_TYPE); encoded.extend_from_slice(public_key); - encoded.push(0x01); + encoded.push(SIGNATURE_TYPE); encoded.extend_from_slice(signature); @@ -259,7 +246,7 @@ impl ExtrinsicPayload { encoded.extend_from_slice(&self.call_index); - encoded.push(0x00); + encoded.push(PUBLIC_KEY_TYPE); encoded.extend_from_slice(&self.destination); encoded.extend_from_slice(&self.value); From e33ca88ea124accdab461f1a325d8420267cc34c Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Tue, 28 Jan 2025 11:53:31 -0300 Subject: [PATCH 06/11] chore: enable substrate chains --- packages/kos/src/chains/mod.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/kos/src/chains/mod.rs b/packages/kos/src/chains/mod.rs index c8fe7e5..d49149a 100644 --- a/packages/kos/src/chains/mod.rs +++ b/packages/kos/src/chains/mod.rs @@ -369,14 +369,14 @@ impl ChainRegistry { constants::DOT, ChainInfo { factory: || Box::new(substrate::Substrate::new(21, 0, "DOT", "Polkadot")), - supported: false, + supported: true, }, ), ( constants::KSM, ChainInfo { factory: || Box::new(substrate::Substrate::new(27, 2, "KSM", "Kusama")), - supported: false, + supported: true, }, ), ( @@ -390,28 +390,28 @@ impl ChainRegistry { constants::REEF, ChainInfo { factory: || Box::new(substrate::Substrate::new(29, 42, "REEF", "Reef")), - supported: false, + supported: true, }, ), ( constants::SDN, ChainInfo { factory: || Box::new(substrate::Substrate::new(35, 5, "SDN", "Shiden")), - supported: false, + supported: true, }, ), ( constants::ASTR, ChainInfo { factory: || Box::new(substrate::Substrate::new(36, 5, "ASTR", "Astar")), - supported: false, + supported: true, }, ), ( constants::CFG, ChainInfo { factory: || Box::new(substrate::Substrate::new(47, 36, "CFG", "Centrifuge")), - supported: false, + supported: true, }, ), ( @@ -425,14 +425,14 @@ impl ChainRegistry { constants::KILT, ChainInfo { factory: || Box::new(substrate::Substrate::new(44, 38, "KILT", "KILT")), - supported: false, + supported: true, }, ), ( constants::ALTAIR, ChainInfo { factory: || Box::new(substrate::Substrate::new(42, 136, "ALTAIR", "Altair")), - supported: false, + supported: true, }, ), ( @@ -572,7 +572,7 @@ impl ChainRegistry { constants::AVAIL, ChainInfo { factory: || Box::new(substrate::Substrate::new(62, 42, "AVAIL", "Avail")), - supported: false, + supported: true, }, ), ( From fee36c7c547e88852cd9f23cbe43db836d830f84 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Wed, 29 Jan 2025 16:00:29 -0300 Subject: [PATCH 07/11] fix: changed substrate tx encoding method by options --- packages/kos/src/chains/mod.rs | 10 +++ packages/kos/src/chains/substrate/mod.rs | 95 +++++++++++++++++++-- packages/kos/src/chains/substrate/models.rs | 84 ++---------------- 3 files changed, 105 insertions(+), 84 deletions(-) diff --git a/packages/kos/src/chains/mod.rs b/packages/kos/src/chains/mod.rs index d49149a..fc59c4c 100644 --- a/packages/kos/src/chains/mod.rs +++ b/packages/kos/src/chains/mod.rs @@ -263,6 +263,16 @@ pub enum ChainOptions { prev_scripts: Vec>, input_amounts: Vec, }, + SUBSTRATE { + call: Vec, + era: Vec, + nonce: u32, + tip: u8, + block_hash: Vec, + genesis_hash: Vec, + spec_version: u32, + transaction_version: u32, + }, } #[allow(dead_code)] diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index db1f32d..6d21437 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -2,7 +2,7 @@ mod models; use crate::chains::substrate::models::ExtrinsicPayload; use crate::chains::util::private_key_from_vec; -use crate::chains::{Chain, ChainError, Transaction, TxInfo}; +use crate::chains::{Chain, ChainError, ChainOptions, Transaction, TxInfo}; use crate::crypto::hash::{blake2b_64_digest, blake2b_digest}; use crate::crypto::sr25519::Sr25519Trait; use crate::crypto::{b58, bip32, sr25519}; @@ -109,7 +109,46 @@ impl Chain for Substrate { private_key: Vec, mut tx: Transaction, ) -> Result { - let extrinsic = ExtrinsicPayload::from_raw(tx.raw_data.clone())?; + let options = tx.options.clone().ok_or(ChainError::MissingOptions)?; + + let extrinsic = match options { + ChainOptions::SUBSTRATE { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + } => { + let genesis_hash: [u8; 32] = genesis_hash + .as_slice() + .try_into() + .map_err(|_| ChainError::InvalidOptions)?; + + let block_hash: [u8; 32] = block_hash + .as_slice() + .try_into() + .map_err(|_| ChainError::InvalidOptions)?; + + ExtrinsicPayload { + call, + era, + nonce, + tip, + mode: 0, + spec_version, + transaction_version, + genesis_hash, + block_hash, + metadata_hash: 0, + } + } + _ => { + return Err(ChainError::InvalidOptions); + } + }; let signature = { let full_unsigned_payload_scale_bytes = tx.raw_data.clone(); @@ -163,8 +202,40 @@ impl Chain for Substrate { } } +fn new_substrate_transaction_options( + call: String, + era: String, + nonce: String, + tip: String, + block_hash: String, + genesis_hash: String, + spec_version: String, + transaction_version: String, +) -> ChainOptions { + let call = hex::decode(call).unwrap(); + let era = hex::decode(era).unwrap(); + let nonce: u32 = nonce.parse().unwrap(); + let tip: u8 = tip.parse().unwrap(); + let block_hash = hex::decode(block_hash).unwrap(); + let genesis_hash = hex::decode(genesis_hash).unwrap(); + let spec_version: u32 = spec_version.parse().unwrap(); + let transaction_version: u32 = transaction_version.parse().unwrap(); + + ChainOptions::SUBSTRATE { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + } +} + #[cfg(test)] mod test { + use crate::chains::substrate::new_substrate_transaction_options; use crate::chains::{Chain, Transaction}; use crate::crypto::base64::simple_base64_decode; use alloc::string::{String, ToString}; @@ -211,21 +282,33 @@ mod test { #[test] fn sign_tx() { - let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); + let dot = super::Substrate::new(27, 2, "Kusama", "KSM"); - let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); let path = dot.get_path(0, false); let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); let pvk = dot.derive(seed, path).unwrap(); - let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHCRAXUDYAAA/E0PABoAAACRsXG7FY4tOEj6I6nxwlGC+44gMTssHrSSGdp6cM6Qw36KXLq5dgOoEvZRpzirvfO3HDN6fM3bEwtF1XTUSlrGAA==").unwrap(); + let raw_data = simple_base64_decode("BgMATg7dBMR7Gtw7IdzYZxpdkKHC63X7YNKTqQhvJibbzVkEJQEcAAAoAAAAAQAAALkXRrReA0bML4FaUgucbLTVwJAq+EjbCoD4WTLS6CdqrE2opg7XFpDCJ63rn+zxU3cs7DhW6Sm5cCF02Gg1wDY=").unwrap(); + + let options = new_substrate_transaction_options( + "0403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904".to_string(), + "4502".to_string(), + "87".to_string(), + "0".to_string(), + "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe".to_string(), + "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe".to_string(), + "1003003".to_string(), + "26".to_string(), + ); let tx = Transaction { raw_data, signature: Vec::new(), tx_hash: Vec::new(), - options: None, + options: Some(options), }; let signed_tx = dot.sign_tx(pvk, tx).unwrap(); diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index b17b825..30fa96c 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -1,4 +1,3 @@ -use crate::chains::ChainError; use crate::crypto::bignum::U256; use alloc::vec; use alloc::vec::Vec; @@ -146,13 +145,9 @@ impl Decode for UIntCompact { /// This structure contains all the necessary fields required for transaction signing. #[allow(dead_code)] pub struct ExtrinsicPayload { - /// The call index identifying the function being called (module index + function index) - pub call_index: [u8; 2], - /// The destination account's public key or address - pub destination: [u8; 32], - pub value: [u8; 2], - pub era: [u8; 2], - pub nonce: [u8; 1], + pub call: Vec, + pub era: Vec, + pub nonce: u32, pub tip: u8, pub mode: u8, pub spec_version: u32, @@ -163,68 +158,6 @@ pub struct ExtrinsicPayload { } impl ExtrinsicPayload { - pub fn from_raw(bytes: Vec) -> Result { - let mut input = bytes.as_slice(); - - let mut call_index = [0u8; 2]; - call_index.copy_from_slice(&input[0..2]); - input = &input[2..]; - - let mut destination = [0u8; 32]; - destination.copy_from_slice(&input[1..33]); - input = &input[33..]; - - let mut value = [0u8; 2]; - value.copy_from_slice(&input[0..2]); - input = &input[2..]; - - let mut era = [0u8; 2]; - era.copy_from_slice(&input[0..2]); - input = &input[2..]; - - let mut nonce = [0u8; 1]; - nonce.copy_from_slice(&input[0..1]); - input = &input[1..]; - - let tip = input[0]; - input = &input[1..]; - - let mode = input[0]; - input = &input[1..]; - - let spec_version = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); - input = &input[4..]; - - let transaction_version = u32::from_le_bytes([input[0], input[1], input[2], input[3]]); - input = &input[4..]; - - let mut genesis_hash = [0u8; 32]; - genesis_hash.copy_from_slice(&input[0..32]); - input = &input[32..]; - - let mut block_hash = [0u8; 32]; - - block_hash.copy_from_slice(&input[0..32]); - input = &input[32..]; - - let metadata_hash = if !input.is_empty() { input[0] } else { 0 }; - - Ok(ExtrinsicPayload { - call_index, - destination, - value, - era, - nonce, - tip, - mode, - spec_version, - transaction_version, - genesis_hash, - block_hash, - metadata_hash, - }) - } - /// Encodes the payload with a signature using the Substrate transaction format. /// The format is: length + (version + signature + era + nonce + tip + call + params) pub fn encode_with_signature(&self, public_key: &[u8; 32], signature: &[u8]) -> Vec { @@ -240,16 +173,11 @@ impl ExtrinsicPayload { encoded.extend_from_slice(signature); encoded.extend_from_slice(&self.era); - encoded.extend_from_slice(self.nonce.as_slice()); - encoded.push(self.tip); + encoded.extend_from_slice(&Compact(self.nonce).encode()); + encoded.extend_from_slice(&Compact(self.tip).encode()); encoded.push(self.mode); - encoded.extend_from_slice(&self.call_index); - - encoded.push(PUBLIC_KEY_TYPE); - - encoded.extend_from_slice(&self.destination); - encoded.extend_from_slice(&self.value); + encoded.extend_from_slice(&self.call); let length = Compact(encoded.len() as u32).encode(); let mut complete_encoded = Vec::with_capacity(length.len() + encoded.len()); From 919f3d6c8089bb14d0cc465a92d24aca1a363618 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Wed, 29 Jan 2025 17:20:03 -0300 Subject: [PATCH 08/11] feat: add substrate params builder to kos-web and kos-mobile modules --- packages/kos-mobile/src/lib.rs | 59 ++++++++++++++++++++++ packages/kos-web/src/wallet.rs | 39 +++++++++++++++ packages/kos/src/chains/mod.rs | 7 ++- packages/kos/src/chains/substrate/mod.rs | 63 ++++++++---------------- packages/kos/src/chains/util.rs | 5 ++ 5 files changed, 129 insertions(+), 44 deletions(-) diff --git a/packages/kos-mobile/src/lib.rs b/packages/kos-mobile/src/lib.rs index 58d2da4..f7eb1af 100644 --- a/packages/kos-mobile/src/lib.rs +++ b/packages/kos-mobile/src/lib.rs @@ -2,6 +2,7 @@ pub mod number; use hex::FromHexError; use hex::ToHex; +use kos::chains::util::hex_string_to_vec; use kos::chains::{ create_custom_evm, get_chain_by_base_id, Chain, ChainError, ChainOptions, Transaction, }; @@ -60,6 +61,45 @@ enum TransactionChainOptions { prev_scripts: Vec>, input_amounts: Vec, }, + Substrate { + call: Vec, + era: Vec, + nonce: u32, + tip: u8, + block_hash: Vec, + genesis_hash: Vec, + spec_version: u32, + transaction_version: u32, + }, +} + +#[allow(clippy::too_many_arguments)] +#[uniffi::export] +fn new_substrate_transaction_options( + call: String, + era: String, + nonce: u32, + tip: u8, + block_hash: String, + genesis_hash: String, + spec_version: u32, + transaction_version: u32, +) -> TransactionChainOptions { + let call = hex_string_to_vec(call.as_str()).unwrap_or_default(); + let era = hex_string_to_vec(era.as_str()).unwrap_or_default(); + let block_hash = hex_string_to_vec(block_hash.as_str()).unwrap_or_default(); + let genesis_hash = hex_string_to_vec(genesis_hash.as_str()).unwrap_or_default(); + + TransactionChainOptions::Substrate { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + } } #[uniffi::export] @@ -184,6 +224,25 @@ fn sign_transaction( prev_scripts, input_amounts, }), + Some(TransactionChainOptions::Substrate { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + }) => Some(ChainOptions::SUBSTRATE { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + }), None => None, }; diff --git a/packages/kos-web/src/wallet.rs b/packages/kos-web/src/wallet.rs index 0b46425..8894002 100644 --- a/packages/kos-web/src/wallet.rs +++ b/packages/kos-web/src/wallet.rs @@ -6,6 +6,7 @@ use strum::{EnumCount, IntoStaticStr}; use crate::error::Error; use crate::utils::unpack; +use kos::chains::util::hex_string_to_vec; use kos::chains::{get_chain_by_base_id, ChainOptions, Transaction as KosTransaction}; use kos::crypto::base64; use wasm_bindgen::prelude::*; @@ -59,6 +60,44 @@ impl TransactionChainOptions { }, } } + + #[wasm_bindgen(js_name = "newEthereumSignOptions")] + pub fn new_ethereum_sign_options(chain_id: u32) -> TransactionChainOptions { + TransactionChainOptions { + data: ChainOptions::EVM { chain_id }, + } + } + + #[allow(clippy::too_many_arguments)] + #[wasm_bindgen(js_name = "newSubstrateSignOptions")] + pub fn new_substrate_sign_options( + call: String, + era: String, + nonce: u32, + tip: u8, + block_hash: String, + genesis_hash: String, + spec_version: u32, + transaction_version: u32, + ) -> TransactionChainOptions { + let call = hex_string_to_vec(call.as_str()).unwrap_or_default(); + let era = hex_string_to_vec(era.as_str()).unwrap_or_default(); + let block_hash = hex_string_to_vec(block_hash.as_str()).unwrap_or_default(); + let genesis_hash = hex_string_to_vec(genesis_hash.as_str()).unwrap_or_default(); + + TransactionChainOptions { + data: ChainOptions::SUBSTRATE { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + }, + } + } } #[wasm_bindgen] diff --git a/packages/kos/src/chains/mod.rs b/packages/kos/src/chains/mod.rs index fc59c4c..73195eb 100644 --- a/packages/kos/src/chains/mod.rs +++ b/packages/kos/src/chains/mod.rs @@ -31,7 +31,7 @@ mod sol; mod substrate; mod sui; pub mod trx; -mod util; +pub mod util; mod xrp; #[derive(Debug)] @@ -58,6 +58,7 @@ pub enum ChainError { InvalidData(String), MissingOptions, InvalidOptions, + InvalidHex, } impl Display for ChainError { @@ -123,6 +124,9 @@ impl Display for ChainError { ChainError::InvalidOptions => { write!(f, "invalid option") } + ChainError::InvalidHex => { + write!(f, "invalid hex") + } } } } @@ -213,6 +217,7 @@ impl ChainError { ChainError::InvalidData(_) => 20, ChainError::MissingOptions => 21, ChainError::InvalidOptions => 22, + ChainError::InvalidHex => 23, } } } diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index 6d21437..67623af 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -202,41 +202,9 @@ impl Chain for Substrate { } } -fn new_substrate_transaction_options( - call: String, - era: String, - nonce: String, - tip: String, - block_hash: String, - genesis_hash: String, - spec_version: String, - transaction_version: String, -) -> ChainOptions { - let call = hex::decode(call).unwrap(); - let era = hex::decode(era).unwrap(); - let nonce: u32 = nonce.parse().unwrap(); - let tip: u8 = tip.parse().unwrap(); - let block_hash = hex::decode(block_hash).unwrap(); - let genesis_hash = hex::decode(genesis_hash).unwrap(); - let spec_version: u32 = spec_version.parse().unwrap(); - let transaction_version: u32 = transaction_version.parse().unwrap(); - - ChainOptions::SUBSTRATE { - call, - era, - nonce, - tip, - block_hash, - genesis_hash, - spec_version, - transaction_version, - } -} - #[cfg(test)] mod test { - use crate::chains::substrate::new_substrate_transaction_options; - use crate::chains::{Chain, Transaction}; + use crate::chains::{Chain, ChainOptions, Transaction}; use crate::crypto::base64::simple_base64_decode; use alloc::string::{String, ToString}; @@ -293,16 +261,25 @@ mod test { let raw_data = simple_base64_decode("BgMATg7dBMR7Gtw7IdzYZxpdkKHC63X7YNKTqQhvJibbzVkEJQEcAAAoAAAAAQAAALkXRrReA0bML4FaUgucbLTVwJAq+EjbCoD4WTLS6CdqrE2opg7XFpDCJ63rn+zxU3cs7DhW6Sm5cCF02Gg1wDY=").unwrap(); - let options = new_substrate_transaction_options( - "0403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904".to_string(), - "4502".to_string(), - "87".to_string(), - "0".to_string(), - "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe".to_string(), - "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe".to_string(), - "1003003".to_string(), - "26".to_string(), - ); + let options = ChainOptions::SUBSTRATE { + call: hex::decode( + "0403004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904", + ) + .unwrap(), + era: hex::decode("4502").unwrap(), + nonce: 87, + tip: 0, + block_hash: hex::decode( + "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + ) + .unwrap(), + genesis_hash: hex::decode( + "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", + ) + .unwrap(), + spec_version: 1003003, + transaction_version: 26, + }; let tx = Transaction { raw_data, diff --git a/packages/kos/src/chains/util.rs b/packages/kos/src/chains/util.rs index 2ed63ab..637cc3b 100644 --- a/packages/kos/src/chains/util.rs +++ b/packages/kos/src/chains/util.rs @@ -13,3 +13,8 @@ pub fn slice_from_vec(vec: &[u8]) -> Result<[u8; N], ChainError> pub fn private_key_from_vec(vec: &[u8]) -> Result<[u8; N], ChainError> { slice_from_vec::(vec).map_err(|_| ChainError::InvalidPrivateKey) } + +pub fn hex_string_to_vec(hex: &str) -> Result, ChainError> { + let hex = hex.trim_start_matches("0x"); + hex::decode(hex).map_err(|_| ChainError::InvalidHex) +} From 36ee7c8d808bae92e4d13d30020d7bcdd149a1b3 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Thu, 30 Jan 2025 17:10:41 -0300 Subject: [PATCH 09/11] chore: improve error handling in substrate sign options. --- packages/kos-web/src/wallet.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/kos-web/src/wallet.rs b/packages/kos-web/src/wallet.rs index 8894002..137ddf5 100644 --- a/packages/kos-web/src/wallet.rs +++ b/packages/kos-web/src/wallet.rs @@ -79,13 +79,17 @@ impl TransactionChainOptions { genesis_hash: String, spec_version: u32, transaction_version: u32, - ) -> TransactionChainOptions { - let call = hex_string_to_vec(call.as_str()).unwrap_or_default(); - let era = hex_string_to_vec(era.as_str()).unwrap_or_default(); - let block_hash = hex_string_to_vec(block_hash.as_str()).unwrap_or_default(); - let genesis_hash = hex_string_to_vec(genesis_hash.as_str()).unwrap_or_default(); - - TransactionChainOptions { + ) -> Result { + let call = hex_string_to_vec(call.as_str()) + .map_err(|e| Error::WalletManager(format!("Invalid call hex: {}", e)))?; + let era = hex_string_to_vec(era.as_str()) + .map_err(|e| Error::WalletManager(format!("Invalid era hex: {}", e)))?; + let block_hash = hex_string_to_vec(block_hash.as_str()) + .map_err(|e| Error::WalletManager(format!("Invalid block hash hex: {}", e)))?; + let genesis_hash = hex_string_to_vec(genesis_hash.as_str()) + .map_err(|e| Error::WalletManager(format!("Invalid genesis hash hex: {}", e)))?; + + Ok(TransactionChainOptions { data: ChainOptions::SUBSTRATE { call, era, @@ -96,7 +100,7 @@ impl TransactionChainOptions { spec_version, transaction_version, }, - } + }) } } From b1f33de5da96533af54d482c6549854424561872 Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Fri, 31 Jan 2025 14:48:40 -0300 Subject: [PATCH 10/11] fix: handle AVAIL transaction app_id when encoding extrinsic --- packages/kos-mobile/src/lib.rs | 5 + packages/kos-web/src/wallet.rs | 2 + packages/kos/src/chains/mod.rs | 3 +- packages/kos/src/chains/substrate/mod.rs | 104 +++++++++++++++++++- packages/kos/src/chains/substrate/models.rs | 9 +- 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/packages/kos-mobile/src/lib.rs b/packages/kos-mobile/src/lib.rs index f7eb1af..b7cd14d 100644 --- a/packages/kos-mobile/src/lib.rs +++ b/packages/kos-mobile/src/lib.rs @@ -70,6 +70,7 @@ enum TransactionChainOptions { genesis_hash: Vec, spec_version: u32, transaction_version: u32, + app_id: Option, }, } @@ -84,6 +85,7 @@ fn new_substrate_transaction_options( genesis_hash: String, spec_version: u32, transaction_version: u32, + app_id: Option, ) -> TransactionChainOptions { let call = hex_string_to_vec(call.as_str()).unwrap_or_default(); let era = hex_string_to_vec(era.as_str()).unwrap_or_default(); @@ -99,6 +101,7 @@ fn new_substrate_transaction_options( genesis_hash, spec_version, transaction_version, + app_id, } } @@ -233,6 +236,7 @@ fn sign_transaction( genesis_hash, spec_version, transaction_version, + app_id, }) => Some(ChainOptions::SUBSTRATE { call, era, @@ -242,6 +246,7 @@ fn sign_transaction( genesis_hash, spec_version, transaction_version, + app_id, }), None => None, }; diff --git a/packages/kos-web/src/wallet.rs b/packages/kos-web/src/wallet.rs index 137ddf5..cc7d1f2 100644 --- a/packages/kos-web/src/wallet.rs +++ b/packages/kos-web/src/wallet.rs @@ -79,6 +79,7 @@ impl TransactionChainOptions { genesis_hash: String, spec_version: u32, transaction_version: u32, + app_id: Option, ) -> Result { let call = hex_string_to_vec(call.as_str()) .map_err(|e| Error::WalletManager(format!("Invalid call hex: {}", e)))?; @@ -99,6 +100,7 @@ impl TransactionChainOptions { genesis_hash, spec_version, transaction_version, + app_id, }, }) } diff --git a/packages/kos/src/chains/mod.rs b/packages/kos/src/chains/mod.rs index 73195eb..80143fe 100644 --- a/packages/kos/src/chains/mod.rs +++ b/packages/kos/src/chains/mod.rs @@ -277,6 +277,7 @@ pub enum ChainOptions { genesis_hash: Vec, spec_version: u32, transaction_version: u32, + app_id: Option, }, } @@ -586,7 +587,7 @@ impl ChainRegistry { ( constants::AVAIL, ChainInfo { - factory: || Box::new(substrate::Substrate::new(62, 42, "AVAIL", "Avail")), + factory: || Box::new(substrate::Substrate::new(62, 42, "Avail", "AVAIL")), supported: true, }, ), diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index 67623af..607e79e 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -121,6 +121,7 @@ impl Chain for Substrate { genesis_hash, spec_version, transaction_version, + app_id, } => { let genesis_hash: [u8; 32] = genesis_hash .as_slice() @@ -143,6 +144,7 @@ impl Chain for Substrate { genesis_hash, block_hash, metadata_hash: 0, + app_id, } } _ => { @@ -249,7 +251,54 @@ mod test { } #[test] - fn sign_tx() { + fn sign_tx_1() { + let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); + + let mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let path = dot.get_path(0, false); + + let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); + let pvk = dot.derive(seed, path).unwrap(); + + let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHAE1QFsAAD8TQ8AGgAAAJGxcbsVji04SPojqfHCUYL7jiAxOywetJIZ2npwzpDDR+4cSO05ZyHnTfHIHpWyqrPhN2Poot7H7VqLlK9MgI0A").unwrap(); + + let options = ChainOptions::SUBSTRATE { + call: hex::decode( + "0503000c2441b8cedbfc7a2edc0968b9a535819969d3e9e0998680babb5827287fc07004", + ) + .unwrap(), + era: hex::decode("d501").unwrap(), + nonce: 27, + tip: 0, + block_hash: hex::decode( + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + ) + .unwrap(), + genesis_hash: hex::decode( + "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + ) + .unwrap(), + spec_version: 1003004, + transaction_version: 26, + app_id: None, + }; + + let tx = Transaction { + raw_data, + signature: Vec::new(), + tx_hash: Vec::new(), + options: Some(options), + }; + + let signed_tx = dot.sign_tx(pvk, tx).unwrap(); + + assert_eq!(signed_tx.signature.len(), 65); + assert_eq!(signed_tx.raw_data.len(), 142); + } + + #[test] + fn sign_tx_2() { let dot = super::Substrate::new(27, 2, "Kusama", "KSM"); let mnemonic = @@ -279,6 +328,7 @@ mod test { .unwrap(), spec_version: 1003003, transaction_version: 26, + app_id: None, }; let tx = Transaction { @@ -294,6 +344,58 @@ mod test { assert_eq!(signed_tx.raw_data.len(), 143); } + #[test] + fn sign_tx_3() { + let dot = super::Substrate::new(62, 42, "Avail", "AVAIL"); + + let mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let path = dot.get_path(0, false); + + let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); + let pvk = dot.derive(seed, path).unwrap(); + + let raw_data = simple_base64_decode("BgMATg7dBMR7Gtw7IdzYZxpdkKHC63X7YNKTqQhvJibbzVkEtQEgAAAoAAAAAQAAALkXRrReA0bML4FaUgucbLTVwJAq+EjbCoD4WTLS6CdqDhX+2GUB2kR8rjtzYfwUoIfzCa63UQhdcamIqku0qBE=").unwrap(); + + let nonce = u32::from_str_radix("0x00000008".trim_start_matches("0x"), 16).unwrap(); + let spec_version = u32::from_str_radix("0x00000028".trim_start_matches("0x"), 16).unwrap(); + let transaction_version = + u32::from_str_radix("0x00000001".trim_start_matches("0x"), 16).unwrap(); + + let options = ChainOptions::SUBSTRATE { + call: hex::decode( + "0603004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5904", + ) + .unwrap(), + era: hex::decode("b501").unwrap(), + nonce, + tip: 0, + block_hash: hex::decode( + "0e15fed86501da447cae3b7361fc14a087f309aeb751085d71a988aa4bb4a811", + ) + .unwrap(), + genesis_hash: hex::decode( + "b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a", + ) + .unwrap(), + spec_version, + transaction_version, + app_id: Some(0), + }; + + let tx = Transaction { + raw_data, + signature: Vec::new(), + tx_hash: Vec::new(), + options: Some(options), + }; + + let signed_tx = dot.sign_tx(pvk, tx).unwrap(); + + assert_eq!(signed_tx.signature.len(), 65); + assert_eq!(signed_tx.raw_data.len(), 142); + } + #[test] fn test_get_tx_info() { let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index 30fa96c..95a6a27 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -155,6 +155,7 @@ pub struct ExtrinsicPayload { pub genesis_hash: [u8; 32], pub block_hash: [u8; 32], pub metadata_hash: u8, + pub app_id: Option, } impl ExtrinsicPayload { @@ -175,7 +176,13 @@ impl ExtrinsicPayload { encoded.extend_from_slice(&self.era); encoded.extend_from_slice(&Compact(self.nonce).encode()); encoded.extend_from_slice(&Compact(self.tip).encode()); - encoded.push(self.mode); + + // Use the app_id if it is set for AVAIL transactions, otherwise use the mode + if let Some(app_id) = self.app_id { + encoded.extend_from_slice(Compact(app_id).encode().as_slice()); + } else { + encoded.push(self.mode); + } encoded.extend_from_slice(&self.call); From 337248fc0815d0582fb2beb234a07f07924e373a Mon Sep 17 00:00:00 2001 From: Pedro Camboim Date: Mon, 10 Feb 2025 08:37:28 -0300 Subject: [PATCH 11/11] feat: support for browser tx --- Cargo.lock | 1 + packages/kos/Cargo.toml | 1 + packages/kos/src/chains/substrate/mod.rs | 126 +++++++++++++++++++- packages/kos/src/chains/substrate/models.rs | 29 +++++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11b9db4..238ad6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1662,6 +1662,7 @@ dependencies = [ "ripemd", "rlp", "schnorrkel", + "serde", "serde_json", "sha2 0.10.8", "sha3", diff --git a/packages/kos/Cargo.toml b/packages/kos/Cargo.toml index 6afd4ee..643e84d 100644 --- a/packages/kos/Cargo.toml +++ b/packages/kos/Cargo.toml @@ -61,6 +61,7 @@ pem = "3" cfb-mode = "0.8" cbc = { version = "0.1", features = ["block-padding", "std"] } pbkdf2 = { version = "0.12", features = ["simple"] } +serde = { version = "1.0.215", features = ["derive"] } [build-dependencies] prost-build = "0.12.1" diff --git a/packages/kos/src/chains/substrate/mod.rs b/packages/kos/src/chains/substrate/mod.rs index 607e79e..5b0680e 100644 --- a/packages/kos/src/chains/substrate/mod.rs +++ b/packages/kos/src/chains/substrate/mod.rs @@ -153,7 +153,7 @@ impl Chain for Substrate { }; let signature = { - let full_unsigned_payload_scale_bytes = tx.raw_data.clone(); + let full_unsigned_payload_scale_bytes = extrinsic.to_bytes(); // If payload is longer than 256 bytes, we hash it and sign the hash instead: if full_unsigned_payload_scale_bytes.len() > 256 { @@ -209,6 +209,67 @@ mod test { use crate::chains::{Chain, ChainOptions, Transaction}; use crate::crypto::base64::simple_base64_decode; use alloc::string::{String, ToString}; + use serde::Deserialize; + + #[derive(Deserialize)] + struct TxBrowser { + #[serde(rename = "specVersion")] + pub spec_version: String, + #[serde(rename = "transactionVersion")] + pub transaction_version: String, + #[serde(rename = "address")] + pub _address: String, + #[serde(rename = "assetId")] + pub _asset_id: Option, + #[serde(rename = "blockHash")] + pub block_hash: String, + #[serde(rename = "blockNumber")] + pub _block_number: String, + pub era: String, + #[serde(rename = "genesisHash")] + pub genesis_hash: String, + #[serde(rename = "metadataHash")] + pub _metadata_hash: Option, + pub method: String, + #[serde(rename = "mode")] + pub _mode: i64, + pub nonce: String, + #[serde(rename = "signedExtensions")] + pub _signed_extensions: Vec, + pub tip: String, + #[serde(rename = "version")] + pub _version: i64, + #[serde(rename = "withSignedTransaction")] + pub _with_signed_transaction: bool, + } + + fn options_from_browser_json(tx: String) -> ChainOptions { + let tx_browser: TxBrowser = serde_json::from_str(&tx).unwrap(); + let call = hex::decode(tx_browser.method.trim_start_matches("0x")).unwrap(); + let era = hex::decode(tx_browser.era.trim_start_matches("0x")).unwrap(); + let nonce = u32::from_str_radix(&tx_browser.nonce.trim_start_matches("0x"), 16).unwrap(); + let tip = u8::from_str_radix(&tx_browser.tip.trim_start_matches("0x"), 16).unwrap(); + let block_hash = hex::decode(tx_browser.block_hash.trim_start_matches("0x")).unwrap(); + let genesis_hash = hex::decode(tx_browser.genesis_hash.trim_start_matches("0x")).unwrap(); + let spec_version = + u32::from_str_radix(&tx_browser.spec_version.trim_start_matches("0x"), 16).unwrap(); + let transaction_version = + u32::from_str_radix(&tx_browser.transaction_version.trim_start_matches("0x"), 16) + .unwrap(); + let app_id = None; + + ChainOptions::SUBSTRATE { + call, + era, + nonce, + tip, + block_hash, + genesis_hash, + spec_version, + transaction_version, + app_id, + } + } #[test] fn test_get_addr() { @@ -247,7 +308,9 @@ mod test { let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); let pvk = dot.derive(seed, path).unwrap(); let payload = [0; 32].to_vec(); - let _tx = dot.sign_raw(pvk, payload).unwrap(); + let sig = dot.sign_raw(pvk, payload).unwrap(); + + assert_eq!(sig.len(), 64); } #[test] @@ -396,6 +459,65 @@ mod test { assert_eq!(signed_tx.raw_data.len(), 142); } + #[test] + fn sign_tx_browser() { + let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); + + let mnemonic = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let path = dot.get_path(1, false); + + let seed = dot.mnemonic_to_seed(mnemonic, String::from("")).unwrap(); + let pvk = dot.derive(seed, path).unwrap(); + + let raw_data = simple_base64_decode("BQMADCRBuM7b/Hou3AlouaU1gZlp0+ngmYaAurtYJyh/wHAE1QFsAAD8TQ8AGgAAAJGxcbsVji04SPojqfHCUYL7jiAxOywetJIZ2npwzpDDR+4cSO05ZyHnTfHIHpWyqrPhN2Poot7H7VqLlK9MgI0A").unwrap(); + + let options = options_from_browser_json( + r#"{ + "specVersion": "0x000f4dfc", + "transactionVersion": "0x0000001a", + "address": "12mM9imBfhL4DfK2Sv9SPi79kKT296YJ6LTT7b7pZuRufXmx", + "assetId": null, + "blockHash": "0x74e061f402b8709b793aede7509ac32ca2bf60ab772035e8de2c3b597a99c3cd", + "blockNumber": "0x017870fe", + "era": "0xe503", + "genesisHash": "0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "metadataHash": null, + "method": "0x0503004e0edd04c47b1adc3b21dcd8671a5d90a1c2eb75fb60d293a9086f2626dbcd5900", + "mode": 0, + "nonce": "0x00000000", + "signedExtensions": [ + "CheckNonZeroSender", + "CheckSpecVersion", + "CheckTxVersion", + "CheckGenesis", + "CheckMortality", + "CheckNonce", + "CheckWeight", + "ChargeTransactionPayment", + "PrevalidateAttests", + "CheckMetadataHash" + ], + "tip": "0x00000000000000000000000000000000", + "version": 4, + "withSignedTransaction": true + }"# + .to_string(), + ); + + let tx = Transaction { + raw_data, + signature: Vec::new(), + tx_hash: Vec::new(), + options: Some(options), + }; + + let signed_tx = dot.sign_tx(pvk, tx).unwrap(); + + assert_eq!(signed_tx.signature.len(), 65); + assert_eq!(signed_tx.raw_data.len(), 142); + } + #[test] fn test_get_tx_info() { let dot = super::Substrate::new(21, 0, "Polkadot", "DOT"); diff --git a/packages/kos/src/chains/substrate/models.rs b/packages/kos/src/chains/substrate/models.rs index 95a6a27..4604dae 100644 --- a/packages/kos/src/chains/substrate/models.rs +++ b/packages/kos/src/chains/substrate/models.rs @@ -159,6 +159,35 @@ pub struct ExtrinsicPayload { } impl ExtrinsicPayload { + /// Encodes the payload using the Substrate transaction format. + /// The format is: version + era + nonce + tip + call + params + pub fn to_bytes(&self) -> Vec { + let mut encoded = Vec::new(); + encoded.extend(self.call.clone()); + encoded.extend(&self.era.clone()); + encoded.extend(Compact(self.nonce).encode()); + encoded.extend(Compact(self.tip).encode()); + + // Use the app_id if it is set for AVAIL transactions, otherwise use the mode + if let Some(app_id) = self.app_id { + encoded.extend(Compact(app_id).encode()); + } else { + encoded.extend(&self.mode.encode()); + } + + encoded.extend(&self.spec_version.encode()); + encoded.extend(&self.transaction_version.encode()); + encoded.extend(&self.genesis_hash); + encoded.extend(&self.block_hash); + + // Use the metadata_hash if it is not set for AVAIL transactions + if self.app_id.is_none() { + encoded.push(self.metadata_hash); + } + + encoded + } + /// Encodes the payload with a signature using the Substrate transaction format. /// The format is: length + (version + signature + era + nonce + tip + call + params) pub fn encode_with_signature(&self, public_key: &[u8; 32], signature: &[u8]) -> Vec {