accumulate_client/protocol/
mod.rs1#![allow(missing_docs)]
7
8use crate::codec::{canonical_json, sha256_bytes};
9use crate::crypto::ed25519_helper::{Ed25519Helper, Keypair};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14pub mod envelope;
15pub mod transaction;
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct TransactionEnvelope {
24 pub signatures: Vec<TransactionSignature>,
25 pub transaction: Vec<Transaction>,
26}
27
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct TransactionSignature {
31 #[serde(rename = "type")]
32 pub signature_type: String,
33 #[serde(rename = "publicKey")]
34 pub public_key: String,
35 pub signature: String,
36 pub signer: String,
37 #[serde(rename = "signerVersion")]
38 pub signer_version: u64,
39 pub timestamp: u64,
40 #[serde(rename = "transactionHash")]
41 pub transaction_hash: String,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct Transaction {
47 pub header: TransactionHeader,
48 pub body: Value,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct TransactionHeader {
54 pub principal: String,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub initiator: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub timestamp: Option<u64>,
59}
60
61#[derive(Debug, Clone, Copy)]
63pub struct EnvelopeBuilder;
64
65impl EnvelopeBuilder {
66 pub fn create_envelope(
68 transaction: Transaction,
69 keypair: &Keypair,
70 signer_url: &str,
71 signer_version: u64,
72 ) -> Result<TransactionEnvelope, EnvelopeError> {
73 let tx_value = serde_json::to_value(&transaction)?;
75 let canonical = canonical_json(&tx_value);
76
77 let tx_hash = sha256_bytes(canonical.as_bytes());
79 let tx_hash_hex = hex::encode(tx_hash);
80
81 let signature = Ed25519Helper::sign_bytes(keypair, &tx_hash);
83
84 let timestamp = SystemTime::now()
86 .duration_since(UNIX_EPOCH)
87 .map_err(|e| EnvelopeError::TimestampError(e.to_string()))?
88 .as_micros() as u64;
89
90 let tx_signature = TransactionSignature {
92 signature_type: "ed25519".to_string(),
93 public_key: hex::encode(Ed25519Helper::public_key_bytes(keypair)),
94 signature: hex::encode(signature.to_bytes()),
95 signer: signer_url.to_string(),
96 signer_version,
97 timestamp,
98 transaction_hash: tx_hash_hex,
99 };
100
101 Ok(TransactionEnvelope {
102 signatures: vec![tx_signature],
103 transaction: vec![transaction],
104 })
105 }
106
107 pub fn create_envelope_from_json(
109 principal: &str,
110 body: Value,
111 keypair: &Keypair,
112 signer_url: &str,
113 signer_version: u64,
114 ) -> Result<TransactionEnvelope, EnvelopeError> {
115 let header = TransactionHeader {
116 principal: principal.to_string(),
117 initiator: None,
118 timestamp: None,
119 };
120
121 let transaction = Transaction { header, body };
122
123 Self::create_envelope(transaction, keypair, signer_url, signer_version)
124 }
125
126 pub fn create_envelope_with_initiator(
128 principal: &str,
129 initiator: &str,
130 body: Value,
131 keypair: &Keypair,
132 signer_url: &str,
133 signer_version: u64,
134 ) -> Result<TransactionEnvelope, EnvelopeError> {
135 let header = TransactionHeader {
136 principal: principal.to_string(),
137 initiator: Some(initiator.to_string()),
138 timestamp: None,
139 };
140
141 let transaction = Transaction { header, body };
142
143 Self::create_envelope(transaction, keypair, signer_url, signer_version)
144 }
145
146 pub fn serialize_envelope(envelope: &TransactionEnvelope) -> Result<String, EnvelopeError> {
148 let value = serde_json::to_value(envelope)?;
149 Ok(canonical_json(&value))
150 }
151
152 pub fn verify_envelope(envelope: &TransactionEnvelope) -> Result<(), EnvelopeError> {
154 if envelope.signatures.is_empty() || envelope.transaction.is_empty() {
155 return Err(EnvelopeError::InvalidEnvelope(
156 "Missing signatures or transactions".to_string(),
157 ));
158 }
159
160 let transaction = &envelope.transaction[0];
161 let signature = &envelope.signatures[0];
162
163 let tx_value = serde_json::to_value(transaction)?;
165 let canonical = canonical_json(&tx_value);
166 let computed_hash = hex::encode(sha256_bytes(canonical.as_bytes()));
167
168 if computed_hash != signature.transaction_hash {
170 return Err(EnvelopeError::HashMismatch {
171 expected: signature.transaction_hash.clone(),
172 computed: computed_hash,
173 });
174 }
175
176 let public_key_bytes = hex::decode(&signature.public_key)
178 .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
179 let signature_bytes = hex::decode(&signature.signature)
180 .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
181
182 if public_key_bytes.len() != 32 || signature_bytes.len() != 64 {
183 return Err(EnvelopeError::InvalidSignature(
184 "Invalid key or signature length".to_string(),
185 ));
186 }
187
188 let mut pk_array = [0u8; 32];
189 let mut sig_array = [0u8; 64];
190 pk_array.copy_from_slice(&public_key_bytes);
191 sig_array.copy_from_slice(&signature_bytes);
192
193 let public_key = Ed25519Helper::public_key_from_bytes(&pk_array)
194 .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
195 let signature_obj = Ed25519Helper::signature_from_bytes(&sig_array)
196 .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
197
198 let tx_hash_bytes = hex::decode(&signature.transaction_hash)
199 .map_err(|e| EnvelopeError::InvalidSignature(e.to_string()))?;
200
201 Ed25519Helper::verify(&public_key, &tx_hash_bytes, &signature_obj)
202 .map_err(|e| EnvelopeError::VerificationFailed(e.to_string()))?;
203
204 Ok(())
205 }
206}
207
208#[derive(Debug, thiserror::Error)]
210pub enum EnvelopeError {
211 #[error("JSON serialization error: {0}")]
212 JsonError(#[from] serde_json::Error),
213
214 #[error("Timestamp error: {0}")]
215 TimestampError(String),
216
217 #[error("Invalid envelope: {0}")]
218 InvalidEnvelope(String),
219
220 #[error("Hash mismatch: expected {expected}, computed {computed}")]
221 HashMismatch { expected: String, computed: String },
222
223 #[error("Invalid signature: {0}")]
224 InvalidSignature(String),
225
226 #[error("Signature verification failed: {0}")]
227 VerificationFailed(String),
228}
229
230pub mod helpers {
232 use super::*;
233 use serde_json::json;
234
235 pub fn create_send_tokens_body(to_url: &str, amount: &str, _token_url: Option<&str>) -> Value {
237 json!({
238 "type": "sendTokens",
239 "to": [{
240 "url": to_url,
241 "amount": amount
242 }]
243 })
244 }
245
246 pub fn create_identity_body(url: &str, public_key_hash: &str) -> Value {
248 json!({
249 "type": "createIdentity",
250 "url": url,
251 "keyBook": {
252 "publicKeyHash": public_key_hash
253 }
254 })
255 }
256
257 pub fn create_add_credits_body(recipient: &str, amount: u64, oracle: Option<&str>) -> Value {
259 json!({
260 "type": "addCredits",
261 "recipient": recipient,
262 "amount": amount,
263 "oracle": oracle.unwrap_or("")
264 })
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use serde_json::json;
272
273 #[test]
274 fn test_envelope_creation() {
275 let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
276 let keypair = Ed25519Helper::keypair_from_hex(hex_key).unwrap();
277
278 let body = helpers::create_send_tokens_body("acc://bob.acme/tokens", "1000", None);
279
280 let envelope = EnvelopeBuilder::create_envelope_from_json(
281 "acc://alice.acme/tokens",
282 body,
283 &keypair,
284 "acc://alice.acme/book/1",
285 1,
286 )
287 .unwrap();
288
289 assert_eq!(envelope.signatures.len(), 1);
290 assert_eq!(envelope.transaction.len(), 1);
291 assert_eq!(envelope.signatures[0].signature_type, "ed25519");
292 assert_eq!(envelope.transaction[0].header.principal, "acc://alice.acme/tokens");
293 }
294
295 #[test]
296 fn test_envelope_serialization() {
297 let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
298 let keypair = Ed25519Helper::keypair_from_hex(hex_key).unwrap();
299
300 let body = helpers::create_send_tokens_body("acc://bob.acme/tokens", "1000", None);
301
302 let envelope = EnvelopeBuilder::create_envelope_from_json(
303 "acc://alice.acme/tokens",
304 body,
305 &keypair,
306 "acc://alice.acme/book/1",
307 1,
308 )
309 .unwrap();
310
311 let serialized = EnvelopeBuilder::serialize_envelope(&envelope).unwrap();
312 assert!(serialized.contains("signatures"));
313 assert!(serialized.contains("transaction"));
314 assert!(serialized.contains("ed25519"));
315 }
316
317 #[test]
318 fn test_envelope_verification() {
319 let hex_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
320 let keypair = Ed25519Helper::keypair_from_hex(hex_key).unwrap();
321
322 let body = helpers::create_send_tokens_body("acc://bob.acme/tokens", "1000", None);
323
324 let envelope = EnvelopeBuilder::create_envelope_from_json(
325 "acc://alice.acme/tokens",
326 body,
327 &keypair,
328 "acc://alice.acme/book/1",
329 1,
330 )
331 .unwrap();
332
333 let result = EnvelopeBuilder::verify_envelope(&envelope);
334 assert!(result.is_ok());
335 }
336
337 #[test]
338 fn test_transaction_helpers() {
339 let send_body = helpers::create_send_tokens_body("acc://recipient", "500", None);
340 assert_eq!(send_body["type"], "sendTokens");
341
342 let identity_body = helpers::create_identity_body("acc://new-identity", "pubkey123");
343 assert_eq!(identity_body["type"], "createIdentity");
344
345 let credits_body = helpers::create_add_credits_body("acc://recipient", 1000, None);
346 assert_eq!(credits_body["type"], "addCredits");
347 }
348}