Skip to main content

chaincraft_rust/examples/
ecdsa_ledger.rs

1//! ECDSA-signed transaction ledger - educational shared object
2//!
3//! Demonstrates ECDSA signature verification for a simple transfer ledger.
4//! Transactions have { from, to, amount, nonce } and are signed by the sender.
5
6use crate::{
7    crypto::ecdsa::{ECDSASignature, ECDSAVerifier},
8    error::{ChaincraftError, Result},
9    shared::{SharedMessage, SharedObjectId},
10    shared_object::ApplicationObject,
11};
12use async_trait::async_trait;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::any::Any;
16use std::collections::{HashMap, HashSet};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "message_type")]
20pub enum LedgerMessageType {
21    #[serde(rename = "TRANSFER")]
22    Transfer {
23        from: String,
24        to: String,
25        amount: u64,
26        nonce: u64,
27        public_key_pem: String,
28        #[serde(default)]
29        signature: String,
30    },
31}
32
33#[derive(Debug, Clone)]
34pub struct LedgerEntry {
35    pub from: String,
36    pub to: String,
37    pub amount: u64,
38    pub nonce: u64,
39}
40
41/// ECDSA-signed transaction ledger.
42/// Accepts TRANSFER messages; validates signature before appending.
43#[derive(Debug, Clone)]
44pub struct ECDSALedgerObject {
45    id: SharedObjectId,
46    entries: Vec<LedgerEntry>,
47    seen_tx_hashes: HashSet<String>,
48    balances: HashMap<String, u64>,
49    nonces: HashMap<String, u64>,
50    verifier: ECDSAVerifier,
51}
52
53impl ECDSALedgerObject {
54    pub fn new() -> Self {
55        Self {
56            id: SharedObjectId::new(),
57            entries: Vec::new(),
58            seen_tx_hashes: HashSet::new(),
59            balances: HashMap::new(),
60            nonces: HashMap::new(),
61            verifier: ECDSAVerifier::new(),
62        }
63    }
64
65    pub fn entries(&self) -> &[LedgerEntry] {
66        &self.entries
67    }
68
69    pub fn balance(&self, account: &str) -> u64 {
70        *self.balances.get(account).unwrap_or(&0)
71    }
72
73    fn tx_hash(from: &str, to: &str, amount: u64, nonce: u64) -> String {
74        format!("{}:{}:{}:{}", from, to, amount, nonce)
75    }
76
77    fn validate_signature(&self, msg_data: &Value, signature: &str, public_key_pem: &str) -> Result<bool> {
78        let mut for_verify = msg_data.clone();
79        if let Some(obj) = for_verify.as_object_mut() {
80            obj.remove("signature");
81        }
82        let payload = serde_json::to_string(&for_verify)
83            .map_err(|e| ChaincraftError::Serialization(crate::error::SerializationError::Json(e)))?;
84        let sig_bytes = hex::decode(signature).map_err(|_| ChaincraftError::validation("Invalid signature hex"))?;
85        let ecdsa_sig = ECDSASignature::from_bytes(&sig_bytes)
86            .map_err(|_| ChaincraftError::validation("Invalid signature format"))?;
87        self.verifier.verify(payload.as_bytes(), &ecdsa_sig, public_key_pem)
88    }
89}
90
91impl Default for ECDSALedgerObject {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97#[async_trait]
98impl ApplicationObject for ECDSALedgerObject {
99    fn id(&self) -> &SharedObjectId {
100        &self.id
101    }
102
103    fn type_name(&self) -> &'static str {
104        "ECDSALedger"
105    }
106
107    async fn is_valid(&self, message: &SharedMessage) -> Result<bool> {
108        let msg: LedgerMessageType = serde_json::from_value(message.data.clone())
109            .map_err(|_| ChaincraftError::validation("Invalid ledger message format"))?;
110
111        match msg {
112            LedgerMessageType::Transfer { from, to, amount, nonce, public_key_pem, signature } => {
113                if signature.is_empty() {
114                    return Ok(false);
115                }
116                let tx_hash = Self::tx_hash(&from, &to, amount, nonce);
117                if self.seen_tx_hashes.contains(&tx_hash) {
118                    return Ok(true);
119                }
120                let msg_data = serde_json::to_value(&message.data).unwrap_or_default();
121                self.validate_signature(&msg_data, &signature, &public_key_pem)
122            }
123        }
124    }
125
126    async fn add_message(&mut self, message: SharedMessage) -> Result<()> {
127        let msg: LedgerMessageType = serde_json::from_value(message.data.clone())
128            .map_err(|_| ChaincraftError::validation("Invalid ledger message format"))?;
129
130        match msg {
131            LedgerMessageType::Transfer {
132                from,
133                to,
134                amount,
135                nonce,
136                public_key_pem,
137                signature,
138            } => {
139                let tx_hash = Self::tx_hash(&from, &to, amount, nonce);
140                if self.seen_tx_hashes.contains(&tx_hash) {
141                    return Ok(());
142                }
143
144                let msg_data = message.data.clone();
145                if !self.validate_signature(&msg_data, &signature, &public_key_pem)? {
146                    return Ok(());
147                }
148
149                let from_balance = *self.balances.get(&from).unwrap_or(&0);
150                let expected_nonce = *self.nonces.get(&from).unwrap_or(&0);
151                if nonce != expected_nonce {
152                    return Ok(());
153                }
154                // Allow first tx from new address (genesis/mint for demo)
155                if from_balance < amount && from_balance > 0 {
156                    return Ok(());
157                }
158
159                self.seen_tx_hashes.insert(tx_hash);
160                self.entries.push(LedgerEntry {
161                    from: from.clone(),
162                    to: to.clone(),
163                    amount,
164                    nonce,
165                });
166                if from_balance >= amount {
167                    self.balances.insert(from.clone(), from_balance - amount);
168                }
169                *self.balances.entry(to).or_insert(0) += amount;
170                self.nonces.insert(from, nonce + 1);
171                Ok(())
172            }
173        }
174    }
175
176    fn is_merkleized(&self) -> bool {
177        false
178    }
179
180    async fn get_latest_digest(&self) -> Result<String> {
181        Ok(self.entries.len().to_string())
182    }
183
184    async fn has_digest(&self, _digest: &str) -> Result<bool> {
185        Ok(false)
186    }
187
188    async fn is_valid_digest(&self, _digest: &str) -> Result<bool> {
189        Ok(true)
190    }
191
192    async fn add_digest(&mut self, _digest: String) -> Result<bool> {
193        Ok(false)
194    }
195
196    async fn gossip_messages(&self, _digest: Option<&str>) -> Result<Vec<SharedMessage>> {
197        Ok(Vec::new())
198    }
199
200    async fn get_messages_since_digest(&self, _digest: &str) -> Result<Vec<SharedMessage>> {
201        Ok(Vec::new())
202    }
203
204    async fn get_state(&self) -> Result<Value> {
205        let balances: HashMap<&str, u64> = self.balances.iter().map(|(k, v)| (k.as_str(), *v)).collect();
206        Ok(serde_json::json!({
207            "entry_count": self.entries.len(),
208            "balances": balances
209        }))
210    }
211
212    async fn reset(&mut self) -> Result<()> {
213        self.entries.clear();
214        self.seen_tx_hashes.clear();
215        self.balances.clear();
216        self.nonces.clear();
217        Ok(())
218    }
219
220    fn clone_box(&self) -> Box<dyn ApplicationObject> {
221        Box::new(self.clone())
222    }
223
224    fn as_any(&self) -> &dyn Any {
225        self
226    }
227
228    fn as_any_mut(&mut self) -> &mut dyn Any {
229        self
230    }
231}
232
233/// Helpers for creating signed transfer messages
234pub mod helpers {
235    use super::*;
236    use crate::crypto::ecdsa::ECDSASigner;
237    use serde_json::json;
238
239    /// Create a signed TRANSFER message
240    pub fn create_transfer(
241        from: String,
242        to: String,
243        amount: u64,
244        nonce: u64,
245        signer: &ECDSASigner,
246    ) -> Result<serde_json::Value> {
247        let public_key_pem = signer.get_public_key_pem()?;
248        let payload = json!({
249            "message_type": "TRANSFER",
250            "from": from,
251            "to": to,
252            "amount": amount,
253            "nonce": nonce,
254            "public_key_pem": public_key_pem,
255            "signature": ""
256        });
257        let mut for_sign = payload.clone();
258        if let Some(obj) = for_sign.as_object_mut() {
259            obj.remove("signature");
260        }
261        let to_sign = serde_json::to_vec(&for_sign)
262            .map_err(|e| ChaincraftError::Serialization(crate::error::SerializationError::Json(e)))?;
263        let sig = signer.sign(&to_sign)?;
264        let sig_hex = hex::encode(sig.to_bytes());
265        let mut out = payload;
266        out["signature"] = serde_json::json!(sig_hex);
267        Ok(out)
268    }
269}