Skip to content

Commit df53204

Browse files
committed
Merge with develop
2 parents 96619c5 + d92f582 commit df53204

File tree

4 files changed

+482
-12
lines changed

4 files changed

+482
-12
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ it as a premier choice for constructing secure and high-performing blockchain so
4646
- [x] Optimism (OP)
4747
- [x] Polygon zkEVM
4848
- [x] Stolz
49+
- [x] Solana (SOL)
4950
- [x] Litecoin (LTC)
5051
- [x] Syscoin (SYS)
5152
- [x] Dogecoin (DOGE)
@@ -60,6 +61,7 @@ it as a premier choice for constructing secure and high-performing blockchain so
6061
- Celestia (TIA)
6162
- Cudos
6263
- Aura
64+
- BNB Chain
6365
- Solana (SOL)
6466
- Bitcoin Cash (BCH)
6567
- Cardano (ADA)

packages/kos/src/chains/mod.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ pub enum ChainError {
6161
InvalidHex,
6262
DecodeRawTx,
6363
DecodeHash,
64+
InvalidTransactionHeader,
65+
InvalidAccountLength,
66+
InvalidBlockhash,
67+
InvalidSignatureLength,
6468
UnsupportedScriptType,
6569
InvalidTransaction(String),
6670
}
@@ -137,6 +141,18 @@ impl Display for ChainError {
137141
ChainError::DecodeHash => {
138142
write!(f, "decode hash")
139143
}
144+
ChainError::InvalidTransactionHeader => {
145+
write!(f, "invalid transaction header")
146+
}
147+
ChainError::InvalidAccountLength => {
148+
write!(f, "invalid account length")
149+
}
150+
ChainError::InvalidBlockhash => {
151+
write!(f, "invalid block hash")
152+
}
153+
ChainError::InvalidSignatureLength => {
154+
write!(f, "invalid signature length")
155+
}
140156
ChainError::UnsupportedScriptType => {
141157
write!(f, "unsupported script type")
142158
}
@@ -236,8 +252,12 @@ impl ChainError {
236252
ChainError::InvalidHex => 23,
237253
ChainError::DecodeRawTx => 24,
238254
ChainError::DecodeHash => 25,
239-
ChainError::UnsupportedScriptType => 26,
240-
ChainError::InvalidTransaction(_) => 27,
255+
ChainError::InvalidTransactionHeader => 26,
256+
ChainError::InvalidAccountLength => 27,
257+
ChainError::InvalidBlockhash => 28,
258+
ChainError::InvalidSignatureLength => 29,
259+
ChainError::UnsupportedScriptType => 30,
260+
ChainError::InvalidTransaction(_) => 31,
241261
}
242262
}
243263
}
@@ -556,7 +576,7 @@ impl ChainRegistry {
556576
constants::SOL,
557577
ChainInfo {
558578
factory: || Box::new(sol::SOL {}),
559-
supported: false,
579+
supported: true,
560580
},
561581
),
562582
(

packages/kos/src/chains/sol/mod.rs

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod models;
2+
13
use crate::chains::util::private_key_from_vec;
24
use crate::chains::{Chain, ChainError, Transaction, TxInfo};
35
use crate::crypto::b58::b58enc;
@@ -24,7 +26,7 @@ impl Chain for SOL {
2426
}
2527

2628
fn get_decimals(&self) -> u32 {
27-
todo!()
29+
9
2830
}
2931

3032
fn mnemonic_to_seed(&self, mnemonic: String, password: String) -> Result<Vec<u8>, ChainError> {
@@ -52,16 +54,44 @@ impl Chain for SOL {
5254
Ok(String::from_utf8(addr)?)
5355
}
5456

55-
fn sign_tx(&self, _private_key: Vec<u8>, _tx: Transaction) -> Result<Transaction, ChainError> {
56-
Err(ChainError::NotSupported)
57+
fn sign_tx(
58+
&self,
59+
private_key: Vec<u8>,
60+
mut tx: Transaction,
61+
) -> Result<Transaction, ChainError> {
62+
let mut sol_tx = models::SolanaTransaction::decode(&tx.raw_data)?;
63+
64+
if sol_tx.message.header.num_required_signatures as usize != 1 {
65+
return Err(ChainError::InvalidTransactionHeader);
66+
}
67+
if sol_tx.message.account_keys.is_empty() {
68+
return Err(ChainError::InvalidAccountLength);
69+
}
70+
if sol_tx.message.recent_blockhash.iter().all(|&x| x == 0)
71+
|| sol_tx.message.recent_blockhash.iter().all(|&x| x == 1)
72+
{
73+
return Err(ChainError::InvalidBlockhash);
74+
}
75+
76+
let message_bytes = sol_tx.message.encode()?;
77+
78+
let signature = self.sign_raw(private_key, message_bytes)?;
79+
if signature.len() != 64 {
80+
return Err(ChainError::InvalidSignatureLength);
81+
}
82+
sol_tx.signatures = vec![signature.clone()];
83+
84+
tx.tx_hash = sol_tx.signatures[0].clone();
85+
86+
let signed_tx = sol_tx.encode()?;
87+
88+
tx.raw_data = signed_tx;
89+
tx.signature = signature;
90+
Ok(tx)
5791
}
5892

59-
fn sign_message(
60-
&self,
61-
_private_key: Vec<u8>,
62-
_message: Vec<u8>,
63-
) -> Result<Vec<u8>, ChainError> {
64-
Err(ChainError::NotSupported)
93+
fn sign_message(&self, private_key: Vec<u8>, message: Vec<u8>) -> Result<Vec<u8>, ChainError> {
94+
self.sign_raw(private_key, message)
6595
}
6696

6797
fn sign_raw(&self, private_key: Vec<u8>, payload: Vec<u8>) -> Result<Vec<u8>, ChainError> {
@@ -79,6 +109,7 @@ impl Chain for SOL {
79109
#[cfg(test)]
80110
mod test {
81111
use super::*;
112+
use crate::crypto::base64::simple_base64_decode;
82113
use alloc::string::ToString;
83114

84115
#[test]
@@ -93,4 +124,148 @@ mod test {
93124
let addr = sol.get_address(pbk).unwrap();
94125
assert_eq!(addr, "B9sVeu4rJU12oUrUtzjc6BSNuEXdfvurZkdcaTVkP2LY");
95126
}
127+
128+
fn create_test_transaction() -> Vec<u8> {
129+
let tx = models::SolanaTransaction {
130+
message: models::Message {
131+
version: "legacy".to_string(),
132+
header: models::MessageHeader {
133+
num_required_signatures: 1,
134+
num_readonly_signed_accounts: 0,
135+
num_readonly_unsigned_accounts: 0,
136+
},
137+
account_keys: vec![
138+
vec![1; 32], // Sender
139+
vec![2; 32], // Recipient
140+
vec![3; 32], // Program ID
141+
],
142+
recent_blockhash: [42; 32],
143+
instructions: vec![models::CompiledInstruction {
144+
program_id_index: 2,
145+
accounts: vec![0, 1],
146+
data: vec![2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0], // Transfer 100 lamports
147+
}],
148+
address_table_lookups: vec![],
149+
},
150+
signatures: vec![],
151+
};
152+
tx.encode().unwrap()
153+
}
154+
155+
#[test]
156+
fn test_derive_and_sign_tx() {
157+
let sol = SOL {};
158+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
159+
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
160+
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();
161+
162+
let raw_tx = create_test_transaction();
163+
let tx = Transaction {
164+
raw_data: raw_tx,
165+
tx_hash: vec![],
166+
signature: vec![],
167+
options: Option::None,
168+
};
169+
170+
let result = sol.sign_tx(pvk, tx).unwrap();
171+
172+
assert_eq!(result.signature.len(), 64);
173+
174+
// Verify tx_hash is not all ones and matches signature
175+
assert!(!result.tx_hash.iter().all(|&x| x == 1));
176+
assert_eq!(result.tx_hash.len(), 64);
177+
assert!(!result.tx_hash.iter().all(|&x| x == 1));
178+
assert!(!result.tx_hash.iter().all(|&x| x == 0));
179+
180+
let decoded = models::SolanaTransaction::decode(&result.raw_data).unwrap();
181+
assert_eq!(decoded.signatures.len(), 1);
182+
assert_eq!(decoded.signatures[0], result.signature);
183+
assert_eq!(decoded.message.header.num_required_signatures, 1);
184+
}
185+
186+
#[test]
187+
fn test_sign_tx_consistent_hash() {
188+
let sol = SOL {};
189+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
190+
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
191+
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();
192+
193+
let raw_tx = create_test_transaction();
194+
let tx1 = Transaction {
195+
raw_data: raw_tx.clone(),
196+
tx_hash: vec![],
197+
signature: vec![],
198+
options: Option::None,
199+
};
200+
201+
let tx2 = Transaction {
202+
raw_data: raw_tx,
203+
tx_hash: vec![],
204+
signature: vec![],
205+
options: Option::None,
206+
};
207+
208+
let result1 = sol.sign_tx(pvk.clone(), tx1).unwrap();
209+
let result2 = sol.sign_tx(pvk, tx2).unwrap();
210+
211+
// Same transaction signed with same key should produce same signature and hash
212+
assert_eq!(result1.signature, result2.signature);
213+
assert_eq!(result1.tx_hash, result2.tx_hash);
214+
}
215+
216+
#[test]
217+
fn test_sign_tx_legacy() {
218+
let sol = SOL {};
219+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
220+
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
221+
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();
222+
223+
let raw_tx = simple_base64_decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIEmjxocK65Bo8r+e3cj7GbPVedpCwx+DCZJ57Tw3fMN0e5dTAYLc651CwBwFga8GLJTsriJc/FAP3GlbhfEGOidAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAACg2vm5+lhfRud/PKY6hEMgdKkQ8I7jtpxDFjknIKRXGQMDAAUCSQIAAAMACQOAlpgAAAAAAAICAAEUAgAAAAEAAAAAAAAAsmBySL6HLBg=").unwrap();
224+
225+
let tx1 = Transaction {
226+
raw_data: raw_tx.clone(),
227+
tx_hash: vec![],
228+
signature: vec![],
229+
options: Option::None,
230+
};
231+
232+
let result = sol.sign_tx(pvk.clone(), tx1).unwrap();
233+
234+
// Same transaction signed with same key should produce same signature and hash
235+
assert_eq!(hex::encode(&result.signature), "b079c666c9ff53bb26d7606d10131ebbc8d398dac9fd1285d5138bbdd521758d7a6b6bdb2876730637704eb1511f3f7d842343b9e406bb3e3583d6588949a904");
236+
}
237+
238+
#[test]
239+
fn test_sign_tx_v0() {
240+
let sol = SOL {};
241+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
242+
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
243+
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();
244+
245+
let raw_tx = simple_base64_decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAGCpo8aHCuuQaPK/nt3I+xmz1XnaQsMfgwmSee08N3zDdHWO9nf7VjXmRzcktw4WtkBVQDTqR6HHs/zYiFPEFdMlR2uAUKvCmGoT5EOvm/TqTTENr0znYcEsWsViKudXw20rGZQgJtALiRcUwlRMT2kZt8QRbvckZEPIiyFe59326vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5egEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpheXoR6gYqo7X4aA9Sx2/Qcpf6TpzF6ddVuj771s5eWQFBgAFAua+AQAGAAkDRJEGAAAAAAAIBQMAEwkECZPxe2T0hK52/wgYCQACAwgTAQcIDxELAAIDDgoNDAkSEhAFI+UXy5d6460qAQAAABlkAAH4LgEAAAAAAMGtCQAAAAAAKwAFCQMDAAABCQEP5d+hcffknhCj1qkbVbtXFKZDtelOHlry/os01b5PsgXi4ePoyQXn5ODlRQ==").unwrap();
246+
let tx1 = Transaction {
247+
raw_data: raw_tx.clone(),
248+
tx_hash: vec![],
249+
signature: vec![],
250+
options: Option::None,
251+
};
252+
253+
let result = sol.sign_tx(pvk.clone(), tx1).unwrap();
254+
// Same transaction signed with same key should produce same signature and hash
255+
assert_eq!(hex::encode(&result.signature), "40098643a37209b2e0984c2f55872ccf150c44a1100a16a985b1bc04b13c31f9d9d1b070229241df5aaa21af22e0e4f88b6371106766fd95096b67f1066f8701");
256+
}
257+
258+
#[test]
259+
fn test_sign_message() {
260+
let sol = SOL {};
261+
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
262+
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
263+
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();
264+
265+
let message = "Hello, World!".as_bytes().to_vec();
266+
let result = sol.sign_message(pvk.clone(), message.into()).unwrap();
267+
268+
// Same transaction signed with same key should produce same signature and hash
269+
assert_eq!(hex::encode(&result), "e8ebd3bf665fe5b57e421c477fa4187ef5f1275ddc8dbf693dd684a0164f11aef22bb98416e2e765d39dbb38451d8996fea135baa9e9fd13890286e8be0e8200");
270+
}
96271
}

0 commit comments

Comments
 (0)