1use ed25519_dalek::{Signature, VerifyingKey};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::fmt;
5
6pub use pot_o_core::TokenType;
7
8pub fn token_discriminant(t: &TokenType) -> u8 {
10 match t {
11 TokenType::TribeChain => 0,
12 TokenType::PTtC => 1,
13 TokenType::NMTC => 2,
14 TokenType::STOMP => 3,
15 TokenType::AUM => 4,
16 TokenType::AI3 => 5,
17 TokenType::RAVECOIN => 6,
18 }
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct TransferTransaction {
23 pub tx_hash: [u8; 32],
24 pub nonce: u64,
25 pub from: String,
26 pub to: String,
27 pub token: TokenType,
28 pub amount: u64,
29 pub fee: u64,
30 pub signature: Vec<u8>,
31 pub timestamp: u64,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct ProofRewardEntry {
36 pub miner_pubkey: String,
37 pub reward_amount: u64,
38 pub proof_hash: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct CoinbaseTransaction {
43 pub tx_hash: [u8; 32],
44 pub height: u64,
45 pub miner_address: String,
46 pub block_reward: u64,
47 pub proof_rewards: Vec<ProofRewardEntry>,
48 pub signature: Vec<u8>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub enum Transaction {
53 Coinbase(CoinbaseTransaction),
54 Transfer(TransferTransaction),
55}
56
57impl Transaction {
58 pub fn tx_hash(&self) -> &[u8; 32] {
59 match self {
60 Transaction::Coinbase(tx) => &tx.tx_hash,
61 Transaction::Transfer(tx) => &tx.tx_hash,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum TxError {
68 InsufficientBalance,
69 InvalidNonce,
70 InvalidSignature,
71 DuplicateTransaction,
72 InvalidToken,
73 AmountZero,
74 SelfTransfer,
75 FeeTooLow,
76 CoinbaseNotFirst,
77 CoinbaseWrongToken,
78 CoinbaseRewardMismatch,
79 SupplyCapExceeded,
80 CoinbaseImmature,
81 MerkleRootMismatch,
82}
83
84impl fmt::Display for TxError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 TxError::InsufficientBalance => write!(f, "insufficient balance"),
88 TxError::InvalidNonce => write!(f, "invalid nonce"),
89 TxError::InvalidSignature => write!(f, "invalid signature"),
90 TxError::DuplicateTransaction => write!(f, "duplicate transaction"),
91 TxError::InvalidToken => write!(f, "invalid token"),
92 TxError::AmountZero => write!(f, "amount must be greater than zero"),
93 TxError::SelfTransfer => write!(f, "self-transfer not allowed"),
94 TxError::FeeTooLow => write!(f, "fee below minimum"),
95 TxError::CoinbaseNotFirst => write!(f, "coinbase transaction must be first in block"),
96 TxError::CoinbaseWrongToken => {
97 write!(f, "coinbase transaction must use TRIBECHAIN token")
98 }
99 TxError::CoinbaseRewardMismatch => {
100 write!(f, "coinbase reward does not match expected block reward")
101 }
102 TxError::SupplyCapExceeded => write!(f, "supply cap exceeded"),
103 TxError::CoinbaseImmature => write!(f, "coinbase reward is not yet spendable"),
104 TxError::MerkleRootMismatch => write!(f, "transaction merkle root mismatch"),
105 }
106 }
107}
108
109impl std::error::Error for TxError {}
110
111pub fn hash_transfer(
112 from: &str,
113 nonce: u64,
114 to: &str,
115 token: &TokenType,
116 amount: u64,
117 fee: u64,
118 timestamp: u64,
119) -> [u8; 32] {
120 let mut hasher = Sha256::new();
121 hasher.update(from.as_bytes());
122 hasher.update(nonce.to_le_bytes());
123 hasher.update(to.as_bytes());
124 hasher.update([token_discriminant(token)]);
125 hasher.update(amount.to_le_bytes());
126 hasher.update(fee.to_le_bytes());
127 hasher.update(timestamp.to_le_bytes());
128 let result = hasher.finalize();
129 let mut hash = [0u8; 32];
130 hash.copy_from_slice(&result);
131 hash
132}
133
134pub fn hash_coinbase(
135 height: u64,
136 miner_address: &str,
137 block_reward: u64,
138 proof_rewards: &[ProofRewardEntry],
139) -> [u8; 32] {
140 let mut hasher = Sha256::new();
141 hasher.update(height.to_le_bytes());
142 hasher.update(miner_address.as_bytes());
143 hasher.update(block_reward.to_le_bytes());
144 hasher.update((proof_rewards.len() as u64).to_le_bytes());
145 for pr in proof_rewards {
146 hasher.update(pr.miner_pubkey.as_bytes());
147 hasher.update(pr.reward_amount.to_le_bytes());
148 hasher.update(pr.proof_hash.as_bytes());
149 }
150 let result = hasher.finalize();
151 let mut hash = [0u8; 32];
152 hash.copy_from_slice(&result);
153 hash
154}
155
156pub fn verify_transfer_sig(tx: &TransferTransaction) -> Result<(), TxError> {
157 let pubkey_bytes = bs58::decode(&tx.from)
158 .into_vec()
159 .map_err(|_| TxError::InvalidSignature)?;
160 let pubkey_array: [u8; 32] = pubkey_bytes
161 .try_into()
162 .map_err(|_| TxError::InvalidSignature)?;
163 let public_key =
164 VerifyingKey::from_bytes(&pubkey_array).map_err(|_| TxError::InvalidSignature)?;
165 let sig_bytes: [u8; 64] = tx
166 .signature
167 .as_slice()
168 .try_into()
169 .map_err(|_| TxError::InvalidSignature)?;
170 let sig = Signature::from_bytes(&sig_bytes);
171
172 let expected_hash = hash_transfer(
173 &tx.from,
174 tx.nonce,
175 &tx.to,
176 &tx.token,
177 tx.amount,
178 tx.fee,
179 tx.timestamp,
180 );
181
182 tracing::debug!(
183 from = %tx.from,
184 nonce = tx.nonce,
185 to = %tx.to,
186 token = ?tx.token,
187 amount = tx.amount,
188 fee = tx.fee,
189 timestamp = tx.timestamp,
190 expected_hash = %hex::encode(expected_hash),
191 sig_from_tx = %hex::encode(&tx.signature[..]),
192 "verify_transfer_sig computing expected_hash"
193 );
194
195 public_key
196 .verify_strict(&expected_hash[..], &sig)
197 .map_err(|_| TxError::InvalidSignature)
198}
199
200pub fn verify_coinbase_sig(cb: &CoinbaseTransaction) -> Result<(), TxError> {
201 let pubkey_bytes = bs58::decode(&cb.miner_address)
202 .into_vec()
203 .map_err(|_| TxError::InvalidSignature)?;
204 let pubkey_array: [u8; 32] = pubkey_bytes
205 .try_into()
206 .map_err(|_| TxError::InvalidSignature)?;
207 let public_key =
208 VerifyingKey::from_bytes(&pubkey_array).map_err(|_| TxError::InvalidSignature)?;
209 let sig_bytes: [u8; 64] = cb
210 .signature
211 .as_slice()
212 .try_into()
213 .map_err(|_| TxError::InvalidSignature)?;
214 let sig = Signature::from_bytes(&sig_bytes);
215
216 let expected_hash = hash_coinbase(
217 cb.height,
218 &cb.miner_address,
219 cb.block_reward,
220 &cb.proof_rewards,
221 );
222 public_key
223 .verify_strict(&expected_hash[..], &sig)
224 .map_err(|_| TxError::InvalidSignature)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use ed25519_dalek::{Signer, SigningKey};
231
232 #[test]
233 fn test_token_discriminant() {
234 assert_eq!(token_discriminant(&TokenType::TribeChain), 0);
235 assert_eq!(token_discriminant(&TokenType::PTtC), 1);
236 assert_eq!(token_discriminant(&TokenType::NMTC), 2);
237 assert_eq!(token_discriminant(&TokenType::STOMP), 3);
238 assert_eq!(token_discriminant(&TokenType::AUM), 4);
239 assert_eq!(token_discriminant(&TokenType::AI3), 5);
240 }
241
242 #[test]
243 fn test_tx_error_display() {
244 assert_eq!(
245 TxError::InsufficientBalance.to_string(),
246 "insufficient balance"
247 );
248 assert_eq!(TxError::InvalidNonce.to_string(), "invalid nonce");
249 assert_eq!(
250 TxError::AmountZero.to_string(),
251 "amount must be greater than zero"
252 );
253 assert_eq!(
254 TxError::CoinbaseImmature.to_string(),
255 "coinbase reward is not yet spendable"
256 );
257 assert_eq!(
258 TxError::MerkleRootMismatch.to_string(),
259 "transaction merkle root mismatch"
260 );
261 }
262
263 #[test]
264 fn test_hash_transfer_deterministic() {
265 let h1 = hash_transfer("Alice", 1, "Bob", &TokenType::TribeChain, 100, 1, 0);
266 let h2 = hash_transfer("Alice", 1, "Bob", &TokenType::TribeChain, 100, 1, 0);
267 assert_eq!(h1, h2);
268 assert_eq!(h1.len(), 32);
269 }
270
271 #[test]
272 fn test_hash_transfer_different_nonce() {
273 let h1 = hash_transfer("Alice", 1, "Bob", &TokenType::TribeChain, 100, 1, 0);
274 let h2 = hash_transfer("Alice", 2, "Bob", &TokenType::TribeChain, 100, 1, 0);
275 assert_ne!(h1, h2);
276 }
277
278 #[test]
279 fn test_hash_coinbase_deterministic() {
280 let pr = vec![];
281 let h1 = hash_coinbase(42, "MinerPubkey", 500, &pr);
282 let h2 = hash_coinbase(42, "MinerPubkey", 500, &pr);
283 assert_eq!(h1, h2);
284 assert_eq!(h1.len(), 32);
285 }
286
287 #[test]
288 fn test_hash_coinbase_different_height() {
289 let pr = vec![];
290 let h1 = hash_coinbase(1, "Miner", 500, &pr);
291 let h2 = hash_coinbase(2, "Miner", 500, &pr);
292 assert_ne!(h1, h2);
293 }
294
295 #[test]
296 fn test_verify_transfer_sig_valid() {
297 let seed = [42u8; 32];
298 let signing_key = SigningKey::from_bytes(&seed);
299
300 let from = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
301 let nonce = 1u64;
302 let to = bs58::encode([99u8; 32]).into_string();
303 let token = TokenType::TribeChain;
304 let amount = 100u64;
305 let fee = 1u64;
306 let timestamp = 0u64;
307
308 let msg = hash_transfer(&from, nonce, &to, &token, amount, fee, timestamp);
309 let signature = signing_key.sign(&msg).to_bytes().to_vec();
310
311 let tx = TransferTransaction {
312 tx_hash: msg,
313 nonce,
314 from,
315 to,
316 token,
317 amount,
318 fee,
319 signature,
320 timestamp,
321 };
322 let result = verify_transfer_sig(&tx);
323 assert!(result.is_ok());
324 }
325
326 #[test]
327 fn test_verify_transfer_sig_bad_key() {
328 let from = bs58::encode([0u8; 32]).into_string();
329 let signature = vec![0u8; 64];
330 let tx = TransferTransaction {
331 tx_hash: [0u8; 32],
332 nonce: 1,
333 from,
334 to: "to".to_string(),
335 token: TokenType::TribeChain,
336 amount: 100,
337 fee: 1,
338 signature,
339 timestamp: 0,
340 };
341 let result = verify_transfer_sig(&tx);
342 assert_eq!(result, Err(TxError::InvalidSignature));
343 }
344
345 #[test]
346 fn test_verify_transfer_sig_tampered_message() {
347 let seed = [42u8; 32];
348 let signing_key = SigningKey::from_bytes(&seed);
349
350 let from = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
351 let nonce = 1u64;
352 let to = bs58::encode([99u8; 32]).into_string();
353 let token = TokenType::TribeChain;
354 let amount = 100u64;
355 let fee = 1u64;
356 let timestamp = 0u64;
357
358 let msg = hash_transfer(&from, nonce, &to, &token, amount, fee, timestamp);
359 let signature = signing_key.sign(&msg).to_bytes().to_vec();
360
361 let tampered_amount = amount + 1;
362 let tx = TransferTransaction {
363 tx_hash: hash_transfer(&from, nonce, &to, &token, tampered_amount, fee, timestamp),
364 nonce,
365 from,
366 to,
367 token,
368 amount: tampered_amount,
369 fee,
370 signature,
371 timestamp: 0,
372 };
373 let result = verify_transfer_sig(&tx);
374 assert_eq!(result, Err(TxError::InvalidSignature));
375 }
376
377 #[test]
378 fn test_verify_coinbase_sig_valid() {
379 let seed = [99u8; 32];
380 let signing_key = SigningKey::from_bytes(&seed);
381
382 let miner_address = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
383 let height = 42u64;
384 let block_reward = 500u64;
385 let proof_rewards = vec![ProofRewardEntry {
386 miner_pubkey: "miner1".to_string(),
387 reward_amount: 100,
388 proof_hash: "abc123".to_string(),
389 }];
390
391 let msg = hash_coinbase(height, &miner_address, block_reward, &proof_rewards);
392 let signature = signing_key.sign(&msg).to_bytes().to_vec();
393
394 let cb = CoinbaseTransaction {
395 tx_hash: msg,
396 height,
397 miner_address,
398 block_reward,
399 proof_rewards,
400 signature,
401 };
402 let result = verify_coinbase_sig(&cb);
403 assert!(result.is_ok());
404 }
405
406 #[test]
407 fn test_verify_coinbase_sig_invalid_pubkey() {
408 let miner_address = bs58::encode([0u8; 32]).into_string();
409 let signature = vec![0u8; 64];
410 let cb = CoinbaseTransaction {
411 tx_hash: [0u8; 32],
412 height: 42,
413 miner_address,
414 block_reward: 500,
415 proof_rewards: vec![],
416 signature,
417 };
418 let result = verify_coinbase_sig(&cb);
419 assert_eq!(result, Err(TxError::InvalidSignature));
420 }
421
422 #[test]
423 fn test_verify_coinbase_sig_tampered_reward() {
424 let seed = [99u8; 32];
425 let signing_key = SigningKey::from_bytes(&seed);
426
427 let miner_address = bs58::encode(signing_key.verifying_key().to_bytes()).into_string();
428 let height = 42u64;
429 let block_reward = 500u64;
430 let proof_rewards = vec![ProofRewardEntry {
431 miner_pubkey: "miner1".to_string(),
432 reward_amount: 100,
433 proof_hash: "abc123".to_string(),
434 }];
435
436 let msg = hash_coinbase(height, &miner_address, block_reward, &proof_rewards);
437 let signature = signing_key.sign(&msg).to_bytes().to_vec();
438
439 let cb = CoinbaseTransaction {
440 tx_hash: msg,
441 height,
442 miner_address,
443 block_reward: block_reward + 1,
444 proof_rewards,
445 signature,
446 };
447 let result = verify_coinbase_sig(&cb);
448 assert_eq!(result, Err(TxError::InvalidSignature));
449 }
450
451 #[test]
452 fn test_transaction_tx_hash() {
453 let hash1 = [1u8; 32];
454 let hash2 = [2u8; 32];
455 let transfer_tx = TransferTransaction {
456 tx_hash: hash1,
457 nonce: 1,
458 from: "from".to_string(),
459 to: "to".to_string(),
460 token: TokenType::TribeChain,
461 amount: 100,
462 fee: 1,
463 signature: vec![],
464 timestamp: 0,
465 };
466 let coinbase_tx = CoinbaseTransaction {
467 tx_hash: hash2,
468 height: 42,
469 miner_address: "miner".to_string(),
470 block_reward: 500,
471 proof_rewards: vec![],
472 signature: vec![],
473 };
474
475 assert_eq!(Transaction::Transfer(transfer_tx).tx_hash(), &hash1);
476 assert_eq!(Transaction::Coinbase(coinbase_tx).tx_hash(), &hash2);
477 }
478}