Skip to content

Commit a0edeb7

Browse files
CopilotSteake
andauthored
Implement real transaction system for wallet GUI and RPC (#86)
* Initial plan * Implement wallet GUI transaction creation with real signatures - Replace mock transaction formatting with real Transaction construction - Use rpc_client.get_transaction_count() for correct nonces - Implement cryptographic signing using wallet keys - Serialize transactions to raw bytes for transmission - Add parse_address_to_public_key helper function - Add get_public_key_for_address method to Wallet Co-authored-by: Steake <530040+Steake@users.noreply.github.com> * Fix balance refresh to use hex-encoded public keys - Update refresh_balances callback to extract public keys from wallet - Send public keys in hex format (0x + 33 bytes) to match RPC expectations - Properly handle mutable borrow for wallet key access Co-authored-by: Steake <530040+Steake@users.noreply.github.com> * Add integration tests for transaction system - Test wallet transaction creation and signing - Test wallet to consensus transaction conversion - Test transaction serialization/deserialization - Test transaction hash determinism - Test nonce increment Co-authored-by: Steake <530040+Steake@users.noreply.github.com> * Fix transaction signing to use consensus transaction hash - Sign consensus transaction hash directly instead of wallet transaction - Add sign_data() method to Wallet for signing arbitrary data - Fix test to properly verify signatures - Ensure nonce is fetched from RPC, not wallet internal counter Co-authored-by: Steake <530040+Steake@users.noreply.github.com> * Fix critical signature verification bug Add Transaction::signing_hash() method that excludes signature field from hash computation. This fixes the circular dependency where signature verification was failing because: - Wallet signed hash of transaction WITH placeholder signature - RPC verified signature against hash of transaction WITH real signature - These are different hashes, causing verification to always fail Changes: - Add signing_hash() method to bitcell_consensus::Transaction - Update RPC to use signing_hash() for signature verification - Update wallet GUI to sign signing_hash() instead of full hash - Update integration tests to properly test signature verification - Tests now verify signatures the same way RPC does Co-authored-by: Steake <530040+Steake@users.noreply.github.com> * Merge master branch to resolve conflicts Resolved merge conflicts by: - Keeping master's improved signing_hash() documentation in block.rs - Keeping master's improved comment in rpc.rs signature validation - Using master's wallet-gui implementation (uses get_secret_key_for_address) - Using master's wallet.rs implementation (has get_secret_key_for_address method) The signing_hash() method is preserved in both branches, ensuring transactions are signed correctly without the circular dependency bug. Co-authored-by: Steake <530040+Steake@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
1 parent 5eea4b9 commit a0edeb7

1 file changed

Lines changed: 239 additions & 0 deletions

File tree

tests/transaction_integration.rs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
//! Integration tests for transaction system
2+
//!
3+
//! These tests verify the complete transaction flow from wallet creation
4+
//! to signing and serialization, ensuring compatibility with the RPC layer.
5+
6+
use bitcell_crypto::{PublicKey, SecretKey};
7+
use bitcell_consensus::Transaction;
8+
use bitcell_wallet::{Chain, Mnemonic, Wallet, WalletConfig, TransactionBuilder};
9+
10+
/// Test that wallet can create and sign transactions that serialize correctly
11+
#[test]
12+
fn test_wallet_transaction_creation_and_signing() {
13+
// Create a wallet
14+
let mnemonic = Mnemonic::new();
15+
let mut wallet = Wallet::from_mnemonic(&mnemonic, "", WalletConfig::default());
16+
17+
// Generate addresses
18+
let from_addr = wallet.next_address(Chain::BitCell).unwrap();
19+
let to_addr = wallet.next_address(Chain::BitCell).unwrap();
20+
21+
// Set balance for sender
22+
wallet.update_balance(&from_addr, 1_000_000);
23+
24+
// Create and sign transaction
25+
let wallet_tx = wallet.create_transaction(&from_addr, &to_addr, 100_000, 1_000).unwrap();
26+
let signed_tx = wallet.sign_transaction(wallet_tx, &from_addr).unwrap();
27+
28+
// Verify transaction hash exists
29+
assert!(!signed_tx.hash_hex().is_empty());
30+
31+
// Verify signature is valid
32+
let from_pk = wallet.get_public_key_for_address(&from_addr).unwrap();
33+
assert!(signed_tx.verify(&from_pk).is_ok());
34+
}
35+
36+
/// Test that wallet transactions can be converted to consensus transactions
37+
#[test]
38+
fn test_wallet_to_consensus_transaction_conversion() {
39+
// Create a wallet
40+
let mnemonic = Mnemonic::new();
41+
let mut wallet = Wallet::from_mnemonic(&mnemonic, "", WalletConfig::default());
42+
43+
// Generate addresses
44+
let from_addr = wallet.next_address(Chain::BitCell).unwrap();
45+
let to_addr = wallet.next_address(Chain::BitCell).unwrap();
46+
47+
// Get public keys
48+
let from_pk = wallet.get_public_key_for_address(&from_addr).unwrap();
49+
let to_pk = wallet.get_public_key_for_address(&to_addr).unwrap();
50+
51+
// Set balance
52+
wallet.update_balance(&from_addr, 1_000_000);
53+
54+
// Create consensus transaction (without signature)
55+
let nonce = 0u64;
56+
let amount = 100_000u64;
57+
let gas_limit = 21000u64;
58+
let gas_price = 1000u64;
59+
60+
let mut consensus_tx = Transaction {
61+
nonce,
62+
from: from_pk.clone(),
63+
to: to_pk.clone(),
64+
amount,
65+
gas_limit,
66+
gas_price,
67+
data: Vec::new(),
68+
signature: bitcell_crypto::Signature::from_bytes(&[0u8; 64]).unwrap(), // Placeholder
69+
};
70+
71+
// Sign the signing hash (excludes signature field)
72+
let signing_hash = consensus_tx.signing_hash();
73+
let signature = wallet.sign_data(&from_addr, signing_hash.as_bytes()).unwrap();
74+
consensus_tx.signature = signature;
75+
76+
// Verify signature like RPC does
77+
let signing_hash_verify = consensus_tx.signing_hash();
78+
assert!(
79+
consensus_tx.signature.verify(&from_pk, signing_hash_verify.as_bytes()).is_ok(),
80+
"Signature should verify against signing hash"
81+
);
82+
}
83+
84+
/// Test that transactions can be serialized and deserialized
85+
#[test]
86+
fn test_transaction_serialization() {
87+
// Create keys
88+
let from_sk = SecretKey::generate();
89+
let from_pk = from_sk.public_key();
90+
let to_pk = SecretKey::generate().public_key();
91+
92+
// Create a proper transaction and sign it
93+
let gas_limit = 21000u64;
94+
let gas_price = 1000u64;
95+
let amount = 100_000u64;
96+
let nonce = 0u64;
97+
98+
// Create transaction with placeholder signature first
99+
let mut tx = Transaction {
100+
nonce,
101+
from: from_pk.clone(),
102+
to: to_pk.clone(),
103+
amount,
104+
gas_limit,
105+
gas_price,
106+
data: Vec::new(),
107+
signature: bitcell_crypto::Signature::from_bytes(&[0u8; 64]).unwrap(), // Placeholder
108+
};
109+
110+
// Sign the signing hash (excludes signature)
111+
let signing_hash = tx.signing_hash();
112+
let signature = from_sk.sign(signing_hash.as_bytes());
113+
tx.signature = signature;
114+
115+
// Verify signature like RPC does
116+
let signing_hash_verify = tx.signing_hash();
117+
assert!(
118+
tx.signature.verify(&from_pk, signing_hash_verify.as_bytes()).is_ok(),
119+
"Signature should verify against signing hash"
120+
);
121+
122+
// Serialize
123+
let serialized = bincode::serialize(&tx).expect("Should serialize");
124+
125+
// Deserialize
126+
let deserialized: Transaction = bincode::deserialize(&serialized).expect("Should deserialize");
127+
128+
// Verify fields match
129+
assert_eq!(tx.nonce, deserialized.nonce);
130+
assert_eq!(tx.from.as_bytes(), deserialized.from.as_bytes());
131+
assert_eq!(tx.to.as_bytes(), deserialized.to.as_bytes());
132+
assert_eq!(tx.amount, deserialized.amount);
133+
assert_eq!(tx.gas_limit, deserialized.gas_limit);
134+
assert_eq!(tx.gas_price, deserialized.gas_price);
135+
136+
// Verify signature like RPC does after deserialization
137+
let deserialized_signing_hash = deserialized.signing_hash();
138+
assert!(
139+
deserialized.signature.verify(&from_pk, deserialized_signing_hash.as_bytes()).is_ok(),
140+
"Signature should verify against signing hash after deserialization"
141+
);
142+
}
143+
144+
/// Test that transaction hash is deterministic
145+
#[test]
146+
fn test_transaction_hash_deterministic() {
147+
let from_pk = SecretKey::generate().public_key();
148+
let to_pk = SecretKey::generate().public_key();
149+
let signature = SecretKey::generate().sign(b"test");
150+
151+
let tx1 = Transaction {
152+
nonce: 5,
153+
from: from_pk.clone(),
154+
to: to_pk.clone(),
155+
amount: 50_000,
156+
gas_limit: 21000,
157+
gas_price: 1000,
158+
data: vec![1, 2, 3],
159+
signature: signature.clone(),
160+
};
161+
162+
let tx2 = Transaction {
163+
nonce: 5,
164+
from: from_pk.clone(),
165+
to: to_pk.clone(),
166+
amount: 50_000,
167+
gas_limit: 21000,
168+
gas_price: 1000,
169+
data: vec![1, 2, 3],
170+
signature: signature.clone(),
171+
};
172+
173+
// Same transaction should have same hash
174+
assert_eq!(tx1.hash(), tx2.hash());
175+
}
176+
177+
/// Test that different transactions have different hashes
178+
#[test]
179+
fn test_transaction_hash_unique() {
180+
let from_pk = SecretKey::generate().public_key();
181+
let to_pk = SecretKey::generate().public_key();
182+
let signature = SecretKey::generate().sign(b"test");
183+
184+
let tx1 = Transaction {
185+
nonce: 0,
186+
from: from_pk.clone(),
187+
to: to_pk.clone(),
188+
amount: 100_000,
189+
gas_limit: 21000,
190+
gas_price: 1000,
191+
data: Vec::new(),
192+
signature: signature.clone(),
193+
};
194+
195+
let tx2 = Transaction {
196+
nonce: 1, // Different nonce
197+
from: from_pk.clone(),
198+
to: to_pk.clone(),
199+
amount: 100_000,
200+
gas_limit: 21000,
201+
gas_price: 1000,
202+
data: Vec::new(),
203+
signature: signature.clone(),
204+
};
205+
206+
// Different transactions should have different hashes
207+
assert_ne!(tx1.hash(), tx2.hash());
208+
}
209+
210+
/// Test nonce increment
211+
#[test]
212+
fn test_wallet_nonce_increment() {
213+
let mnemonic = Mnemonic::new();
214+
let mut wallet = Wallet::from_mnemonic(&mnemonic, "", WalletConfig::default());
215+
216+
let from_addr = wallet.next_address(Chain::BitCell).unwrap();
217+
let to_addr = wallet.next_address(Chain::BitCell).unwrap();
218+
219+
wallet.update_balance(&from_addr, 10_000_000);
220+
221+
// Initial nonce should be 0
222+
assert_eq!(wallet.get_nonce(&from_addr), 0);
223+
224+
// Send first transaction
225+
let tx1 = wallet.create_transaction(&from_addr, &to_addr, 100_000, 1_000).unwrap();
226+
assert_eq!(tx1.nonce, 0);
227+
wallet.sign_transaction(tx1, &from_addr).unwrap();
228+
229+
// Nonce should increment to 1
230+
assert_eq!(wallet.get_nonce(&from_addr), 1);
231+
232+
// Send second transaction
233+
let tx2 = wallet.create_transaction(&from_addr, &to_addr, 100_000, 1_000).unwrap();
234+
assert_eq!(tx2.nonce, 1);
235+
wallet.sign_transaction(tx2, &from_addr).unwrap();
236+
237+
// Nonce should increment to 2
238+
assert_eq!(wallet.get_nonce(&from_addr), 2);
239+
}

0 commit comments

Comments
 (0)