Skip to main content

pot_o_extensions/
tx.rs

1use ed25519_dalek::{Signature, VerifyingKey};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::fmt;
5
6pub use pot_o_core::TokenType;
7
8/// Stable u8 discriminant for TokenType (used in hash to avoid Display instability).
9pub 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}