1use sha2::{Digest, Sha256};
11
12use crate::error::CommitmentError;
13use crate::{Asset, AssetKind, BigNumber, Chain, Condition, Escrow, ExecutionState, ID};
14
15type Result<T> = std::result::Result<T, CommitmentError>;
16
17const JOURNAL_VERSION: u8 = 1;
18
19const AMOUNT_WIDTH: usize = 32;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ExecutionResult {
25 Success(ExecutionState),
27 Failure,
29}
30
31#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
34pub struct ProofInput {
35 pub escrow: Escrow,
37 pub chain: Chain,
39 pub agent_id: ID,
41 pub escrow_id: u64,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PublicCommitment {
48 pub chain: Chain,
50 pub agent_id: Vec<u8>,
52 pub escrow_id: u64,
54 pub sender: Vec<u8>,
56 pub recipient: Vec<u8>,
58 pub asset_kind: AssetKind,
60 pub asset_token: Vec<u8>,
62 pub amount: BigNumber,
64 pub condition: [u8; 32],
66 pub outcome: ExecutionResult,
68}
69
70impl PublicCommitment {
71 pub fn new(input: &ProofInput, outcome: ExecutionResult) -> crate::Result<Self> {
78 let Asset {
79 kind,
80 agent_id,
81 amount,
82 ..
83 } = &input.escrow.asset;
84
85 let asset_token = agent_id
86 .as_ref()
87 .map(ID::to_bytes)
88 .transpose()?
89 .unwrap_or_default();
90
91 Ok(Self {
92 chain: input.chain,
93 agent_id: input.agent_id.to_bytes()?,
94 escrow_id: input.escrow_id,
95 sender: input.escrow.sender.to_bytes()?,
96 recipient: input.escrow.recipient.to_bytes()?,
97 asset_kind: *kind,
98 asset_token,
99 amount: amount.clone(),
100 condition: condition_commitment(input.escrow.condition.as_ref()),
101 outcome,
102 })
103 }
104
105 pub fn to_journal_bytes(&self) -> Result<Vec<u8>> {
113 let mut out = Vec::new();
114 out.push(JOURNAL_VERSION);
115 out.push(chain_tag(self.chain));
116
117 match self.outcome {
118 ExecutionResult::Success(state) => {
119 out.push(1);
120 out.push(state_tag(state));
121 }
122 ExecutionResult::Failure => out.push(0),
123 }
124
125 push_bytes(&mut out, &self.agent_id)?;
126 out.extend_from_slice(&self.escrow_id.to_be_bytes());
127 push_bytes(&mut out, &self.sender)?;
128 push_bytes(&mut out, &self.recipient)?;
129 out.push(asset_kind_tag(self.asset_kind));
130 push_bytes(&mut out, &self.asset_token)?;
131 out.extend_from_slice(&amount_be_bytes(&self.amount)?);
132 out.extend_from_slice(&self.condition);
133
134 Ok(out)
135 }
136
137 pub fn from_journal_bytes(bytes: &[u8]) -> Result<Self> {
144 let mut reader = Reader::new(bytes);
145
146 let version = reader.u8()?;
147 if version != JOURNAL_VERSION {
148 return Err(CommitmentError::UnknownVersion(version));
149 }
150
151 let chain = chain_from_tag(reader.u8()?)?;
152 let outcome = match reader.u8()? {
153 0 => ExecutionResult::Failure,
154 1 => ExecutionResult::Success(state_from_tag(reader.u8()?)?),
155 other => return Err(CommitmentError::unknown_tag("outcome", other)),
156 };
157
158 let agent_id = reader.length_prefixed()?.to_vec();
159 let escrow_id = reader.u64()?;
160 let sender = reader.length_prefixed()?.to_vec();
161 let recipient = reader.length_prefixed()?.to_vec();
162 let asset_kind = asset_kind_from_tag(reader.u8()?)?;
163 let asset_token = reader.length_prefixed()?.to_vec();
164 let amount = BigNumber(num_bigint::BigUint::from_bytes_be(
165 reader.take(AMOUNT_WIDTH)?,
166 ));
167 let condition = reader.array32()?;
168 reader.finish()?;
169
170 Ok(Self {
171 chain,
172 agent_id,
173 escrow_id,
174 sender,
175 recipient,
176 asset_kind,
177 asset_token,
178 amount,
179 condition,
180 outcome,
181 })
182 }
183
184 pub fn digest(&self) -> Result<[u8; 32]> {
191 Ok(Sha256::digest(self.to_journal_bytes()?).into())
192 }
193}
194
195fn condition_commitment(condition: Option<&Condition>) -> [u8; 32] {
198 condition.map(Condition::commitment).unwrap_or([0u8; 32])
199}
200
201fn amount_be_bytes(amount: &BigNumber) -> Result<[u8; AMOUNT_WIDTH]> {
203 let be = amount.0.to_bytes_be();
204 let offset = AMOUNT_WIDTH
205 .checked_sub(be.len())
206 .ok_or(CommitmentError::AmountTooLarge)?;
207 let mut padded = [0u8; AMOUNT_WIDTH];
208 padded[offset..].copy_from_slice(&be);
209 Ok(padded)
210}
211
212fn push_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<()> {
214 let len = u16::try_from(bytes.len()).map_err(|_| CommitmentError::FieldTooLong(bytes.len()))?;
215 out.extend_from_slice(&len.to_be_bytes());
216 out.extend_from_slice(bytes);
217 Ok(())
218}
219
220fn chain_tag(chain: Chain) -> u8 {
221 match chain {
222 Chain::Ethereum => 0,
223 Chain::Solana => 1,
224 }
225}
226
227fn chain_from_tag(tag: u8) -> Result<Chain> {
228 match tag {
229 0 => Ok(Chain::Ethereum),
230 1 => Ok(Chain::Solana),
231 other => Err(CommitmentError::unknown_tag("chain", other)),
232 }
233}
234
235fn asset_kind_tag(kind: AssetKind) -> u8 {
236 match kind {
237 AssetKind::Native => 0,
238 AssetKind::Token => 1,
239 }
240}
241
242fn asset_kind_from_tag(tag: u8) -> Result<AssetKind> {
243 match tag {
244 0 => Ok(AssetKind::Native),
245 1 => Ok(AssetKind::Token),
246 other => Err(CommitmentError::unknown_tag("asset_kind", other)),
247 }
248}
249
250fn state_tag(state: ExecutionState) -> u8 {
251 match state {
252 ExecutionState::Initialized => 0,
253 ExecutionState::Funded => 1,
254 ExecutionState::ConditionsMet => 2,
255 }
256}
257
258fn state_from_tag(tag: u8) -> Result<ExecutionState> {
259 match tag {
260 0 => Ok(ExecutionState::Initialized),
261 1 => Ok(ExecutionState::Funded),
262 2 => Ok(ExecutionState::ConditionsMet),
263 other => Err(CommitmentError::unknown_tag("execution_state", other)),
264 }
265}
266
267struct Reader<'a> {
269 bytes: &'a [u8],
270 offset: usize,
271}
272
273impl<'a> Reader<'a> {
274 fn new(bytes: &'a [u8]) -> Self {
275 Self { bytes, offset: 0 }
276 }
277
278 fn take(&mut self, len: usize) -> Result<&'a [u8]> {
279 let end = self
280 .offset
281 .checked_add(len)
282 .ok_or(CommitmentError::Truncated(self.offset))?;
283 let slice = self
284 .bytes
285 .get(self.offset..end)
286 .ok_or(CommitmentError::Truncated(self.offset))?;
287 self.offset = end;
288 Ok(slice)
289 }
290
291 fn u8(&mut self) -> Result<u8> {
292 self.take(1).map(|s| s[0])
293 }
294
295 fn u64(&mut self) -> Result<u64> {
296 let bytes: [u8; 8] = self.take(8)?.try_into().expect("slice of length 8");
297 Ok(u64::from_be_bytes(bytes))
298 }
299
300 fn array32(&mut self) -> Result<[u8; 32]> {
301 Ok(self.take(32)?.try_into().expect("slice of length 32"))
302 }
303
304 fn length_prefixed(&mut self) -> Result<&'a [u8]> {
305 let len = u16::from_be_bytes(self.take(2)?.try_into().expect("slice of length 2"));
306 self.take(usize::from(len))
307 }
308
309 fn finish(self) -> Result<()> {
310 (self.offset == self.bytes.len())
311 .then_some(())
312 .ok_or(CommitmentError::TrailingBytes(self.offset))
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::{Party, Result};
320
321 fn sample(condition: Option<Condition>, outcome: ExecutionResult) -> Result<PublicCommitment> {
322 let escrow = Escrow::new(
323 Party::for_chain(
324 Chain::Ethereum,
325 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
326 )?,
327 Party::for_chain(
328 Chain::Ethereum,
329 "0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8",
330 )?,
331 Asset::native(BigNumber::from(1_000_000_000u64)),
332 condition,
333 );
334 let input = ProofInput {
335 escrow,
336 chain: Chain::Ethereum,
337 agent_id: ID::for_chain(
338 Chain::Ethereum,
339 "0x00000000000000000000000000000000DeaDBeef",
340 )?,
341 escrow_id: 42,
342 };
343 PublicCommitment::new(&input, outcome)
344 }
345
346 #[test]
347 fn journal_roundtrip_success() {
348 let commitment = sample(
349 None,
350 ExecutionResult::Success(ExecutionState::ConditionsMet),
351 )
352 .unwrap();
353 let bytes = commitment.to_journal_bytes().unwrap();
354 let decoded = PublicCommitment::from_journal_bytes(&bytes).unwrap();
355 assert_eq!(decoded, commitment);
356 assert_eq!(decoded.escrow_id, 42);
357 assert_eq!(decoded.amount, BigNumber::from(1_000_000_000u64));
358 }
359
360 #[test]
361 fn journal_roundtrip_failure_and_condition() {
362 let preimage = b"zkEscrow".to_vec();
363 let hash = sha2::Sha256::digest(&preimage).into();
364 let condition = Condition::hashlock(hash, preimage);
365 let expected = condition.commitment();
366
367 let commitment = sample(Some(condition), ExecutionResult::Failure).unwrap();
368 assert_eq!(commitment.condition, expected);
369
370 let bytes = commitment.to_journal_bytes().unwrap();
371 let decoded = PublicCommitment::from_journal_bytes(&bytes).unwrap();
372 assert_eq!(decoded, commitment);
373 assert_eq!(decoded.outcome, ExecutionResult::Failure);
374 assert!(decoded.condition != [0u8; 32]);
375 }
376
377 #[test]
378 fn no_condition_commits_zero() {
379 let commitment = sample(
380 None,
381 ExecutionResult::Success(ExecutionState::ConditionsMet),
382 )
383 .unwrap();
384 assert_eq!(commitment.condition, [0u8; 32]);
385 }
386
387 #[test]
388 fn digest_matches_sha256_of_journal() {
389 let commitment = sample(
390 None,
391 ExecutionResult::Success(ExecutionState::ConditionsMet),
392 )
393 .unwrap();
394 let bytes = commitment.to_journal_bytes().unwrap();
395 let expected: [u8; 32] = sha2::Sha256::digest(&bytes).into();
396 assert_eq!(commitment.digest().unwrap(), expected);
397 }
398
399 #[test]
400 fn truncated_journal_is_rejected() {
401 let commitment = sample(
402 None,
403 ExecutionResult::Success(ExecutionState::ConditionsMet),
404 )
405 .unwrap();
406 let bytes = commitment.to_journal_bytes().unwrap();
407 assert!(PublicCommitment::from_journal_bytes(&bytes[..bytes.len() - 1]).is_err());
408 }
409
410 #[test]
411 fn trailing_bytes_are_rejected() {
412 let commitment = sample(
413 None,
414 ExecutionResult::Success(ExecutionState::ConditionsMet),
415 )
416 .unwrap();
417 let mut bytes = commitment.to_journal_bytes().unwrap();
418 bytes.push(0);
419 assert!(PublicCommitment::from_journal_bytes(&bytes).is_err());
420 }
421
422 #[test]
423 fn unknown_version_is_rejected() {
424 let commitment = sample(
425 None,
426 ExecutionResult::Success(ExecutionState::ConditionsMet),
427 )
428 .unwrap();
429 let mut bytes = commitment.to_journal_bytes().unwrap();
430 bytes[0] = 0xFF;
431 assert!(matches!(
432 PublicCommitment::from_journal_bytes(&bytes),
433 Err(CommitmentError::UnknownVersion(0xFF))
434 ));
435 }
436}