1use ethers::abi::{encode, Token};
4use ethers::types::{Address, Bytes, H256, U256};
5use std::sync::{Arc, LazyLock};
6use thiserror::Error;
7use tracing::{debug, info};
8
9#[allow(clippy::expect_used)]
11static BN254_MODULUS: LazyLock<U256> = LazyLock::new(|| {
12 U256::from_dec_str(
13 "21888242871839275222246405745257275088548364400416034343698204186575808495617",
14 )
15 .expect("BN254 modulus constant is a valid decimal string")
16});
17
18use crate::crypto_helpers::field_to_address;
19use crate::economics::{FeeConfig, FeeEstimate, FeeManager, PriceData};
20use crate::note_factory::{ChangeNoteResult, DepositNoteResult, SpendingInputs};
21use crate::note_processor::WalletNote;
22use crate::proof_inputs::{
23 DLEQProof, DepositInputs, GasPaymentInputs, JoinInputs, NotePlaintext, PublicClaimInputs,
24 SplitInputs, TransferInputs, WithdrawInputs,
25};
26use crate::prover::ClientProver;
27use nox_core::traits::interfaces::InfrastructureError;
28
29#[derive(Debug, Error)]
30pub enum BuilderError {
31 #[error("Proof generation failed: {0}")]
32 ProofGeneration(String),
33 #[error("Insufficient funds: need {needed}, have {available}")]
34 InsufficientFunds { needed: U256, available: U256 },
35 #[error("No suitable note found for payment")]
36 NoSuitableNote,
37 #[error("Encoding error: {0}")]
38 Encoding(String),
39 #[error("Configuration error: {0}")]
40 Config(String),
41 #[error("Infrastructure error: {0}")]
42 Infrastructure(#[from] InfrastructureError),
43}
44
45#[derive(Debug, Clone)]
47pub struct GasPaymentBundle {
48 pub proof: Vec<u8>,
49 pub public_inputs: Vec<[u8; 32]>,
50 pub public_inputs_hex: Vec<String>,
51 pub nullifier_hash: H256,
52 pub fee_amount: U256,
53 pub execution_hash: H256,
55}
56
57#[derive(Debug, Clone)]
58pub struct MulticallBundle {
59 pub gas_payment: GasPaymentBundle,
60 pub multicall_data: Bytes,
61 pub multicall_target: Address,
62}
63
64#[derive(Debug, Clone)]
65pub struct DepositProofBundle {
66 pub proof: Vec<u8>,
67 pub public_inputs: Vec<[u8; 32]>,
68 pub deposit: DepositNoteResult,
69 pub calldata: Bytes,
70}
71
72#[derive(Debug, Clone)]
73pub struct WithdrawProofBundle {
74 pub proof: Vec<u8>,
75 pub public_inputs: Vec<[u8; 32]>,
76 pub nullifier_hash: H256,
77 pub change_note: Option<ChangeNoteResult>,
78 pub calldata: Bytes,
79}
80
81#[derive(Debug, Clone)]
82pub struct TransferProofBundle {
83 pub proof: Vec<u8>,
84 pub public_inputs: Vec<[u8; 32]>,
85 pub nullifier_hash: H256,
86 pub memo_commitment: U256,
87 pub change_commitment: U256,
88 pub transfer_tag: U256,
90 pub calldata: Bytes,
91}
92
93#[derive(Debug, Clone)]
94pub struct SplitProofBundle {
95 pub proof: Vec<u8>,
96 pub public_inputs: Vec<[u8; 32]>,
97 pub nullifier_hash: H256,
98 pub note_out_1: ChangeNoteResult,
99 pub note_out_2: ChangeNoteResult,
100 pub calldata: Bytes,
101}
102
103#[derive(Debug, Clone)]
104pub struct JoinProofBundle {
105 pub proof: Vec<u8>,
106 pub public_inputs: Vec<[u8; 32]>,
107 pub nullifier_hash_a: H256,
108 pub nullifier_hash_b: H256,
109 pub note_out: ChangeNoteResult,
110 pub calldata: Bytes,
111}
112
113#[derive(Debug, Clone)]
114pub struct PublicClaimProofBundle {
115 pub proof: Vec<u8>,
116 pub public_inputs: Vec<[u8; 32]>,
117 pub note_out: ChangeNoteResult,
118 pub calldata: Bytes,
119}
120
121#[derive(Debug, Clone)]
122pub struct BuilderConfig {
123 pub fee_config: FeeConfig,
124 pub darkpool_address: Address,
125 pub multicall_address: Address,
126 pub compliance_pk: (U256, U256),
127}
128
129impl Default for BuilderConfig {
130 fn default() -> Self {
131 Self {
132 fee_config: FeeConfig::default(),
133 darkpool_address: Address::zero(),
134 multicall_address: Address::zero(),
135 compliance_pk: (U256::zero(), U256::zero()),
136 }
137 }
138}
139
140pub struct TransactionBuilder {
141 prover: Arc<ClientProver>,
142 fee_manager: FeeManager,
143 config: BuilderConfig,
144}
145
146impl TransactionBuilder {
147 #[must_use]
148 pub fn new(prover: Arc<ClientProver>, config: BuilderConfig) -> Self {
149 Self {
150 prover,
151 fee_manager: FeeManager::new(config.fee_config.clone()),
152 config,
153 }
154 }
155
156 #[allow(clippy::too_many_arguments)]
161 pub async fn build_gas_payment(
162 &self,
163 note: &WalletNote,
164 merkle_root: U256,
165 merkle_path: Vec<U256>,
166 payment_amount: U256,
167 relayer_address: Address,
168 execution_hash: H256,
169 pre_created_change: Option<ChangeNoteResult>,
170 current_timestamp: u64,
171 ) -> Result<GasPaymentBundle, BuilderError> {
172 info!(
173 "Building gas payment proof: amount={}, note_value={}",
174 payment_amount, note.note.value
175 );
176
177 if note.note.value < payment_amount {
178 return Err(BuilderError::InsufficientFunds {
179 needed: payment_amount,
180 available: note.note.value,
181 });
182 }
183
184 let (change_note, change_ephemeral_sk) = if let Some(change_result) = pre_created_change {
185 (change_result.note, change_result.ephemeral_sk)
186 } else {
187 let change_value = note.note.value.checked_sub(payment_amount).ok_or(
188 BuilderError::InsufficientFunds {
189 needed: payment_amount,
190 available: note.note.value,
191 },
192 )?;
193 let asset_address = field_to_address(note.note.asset_id);
194 (
195 NotePlaintext::random(change_value, asset_address),
196 generate_random_scalar(),
197 )
198 };
199
200 let inputs = GasPaymentInputs {
201 merkle_root,
202 current_timestamp,
203 payment_value: payment_amount,
204 payment_asset_id: note.note.asset_id,
205 relayer_address,
206 execution_hash: U256::from_big_endian(execution_hash.as_bytes()),
207 compliance_pk: self.config.compliance_pk,
208 old_note: note.note.clone(),
209 old_shared_secret: note.spending_secret,
210 old_note_index: note.leaf_index,
211 old_note_path: merkle_path,
212 hashlock_preimage: U256::zero(),
213 change_note,
214 change_ephemeral_sk,
215 };
216
217 debug!("Generating gas payment ZK proof...");
218 let proof_data = self
219 .prover
220 .prove_gas_payment(&inputs)
221 .await
222 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
223
224 let public_inputs_bytes = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
225 let nullifier_hash = extract_gas_payment_nullifier(&public_inputs_bytes)?;
226
227 info!(
228 "Gas payment proof generated: {} bytes, {} public inputs",
229 proof_data.proof.len(),
230 public_inputs_bytes.len()
231 );
232
233 Ok(GasPaymentBundle {
234 proof: proof_data.proof,
235 public_inputs: public_inputs_bytes,
236 public_inputs_hex: proof_data.public_inputs,
237 nullifier_hash,
238 fee_amount: payment_amount,
239 execution_hash,
240 })
241 }
242
243 #[allow(clippy::too_many_arguments)]
245 pub async fn build_paid_action(
246 &self,
247 note: &WalletNote,
248 merkle_root: U256,
249 merkle_path: Vec<U256>,
250 action_target: Address,
251 action_calldata: Bytes,
252 prices: &PriceData,
253 relayer_address: Address,
254 gas_limit: U256,
255 gas_change_note: Option<ChangeNoteResult>,
256 current_timestamp: u64,
257 ) -> Result<MulticallBundle, BuilderError> {
258 if self.config.multicall_address.is_zero() {
259 return Err(BuilderError::Config(
260 "multicall_address is zero -- BuilderConfig.multicall_address must be set for paid actions".into(),
261 ));
262 }
263
264 let fee_estimate = self.fee_manager.calculate_fee(gas_limit, prices);
265 debug!(
266 "Fee estimate: {} (gas: {}, premium: {}bps)",
267 fee_estimate.fee_amount, fee_estimate.gas_limit, fee_estimate.premium_bps
268 );
269
270 let execution_hash =
271 compute_execution_hash(&action_target, &action_calldata, &fee_estimate.fee_amount);
272
273 let gas_payment = self
274 .build_gas_payment(
275 note,
276 merkle_root,
277 merkle_path,
278 fee_estimate.fee_amount,
279 relayer_address,
280 execution_hash,
281 gas_change_note,
282 current_timestamp,
283 )
284 .await?;
285
286 let multicall_data = encode_multicall(
287 self.config.darkpool_address,
288 &gas_payment.proof,
289 &gas_payment.public_inputs,
290 action_target,
291 action_calldata,
292 )?;
293
294 Ok(MulticallBundle {
295 gas_payment,
296 multicall_data,
297 multicall_target: self.config.multicall_address,
298 })
299 }
300
301 #[must_use]
303 pub fn estimate_fee(&self, gas_limit: U256, prices: &PriceData) -> FeeEstimate {
304 self.fee_manager.calculate_fee(gas_limit, prices)
305 }
306
307 pub async fn build_deposit(
308 &self,
309 deposit_result: &DepositNoteResult,
310 ) -> Result<DepositProofBundle, BuilderError> {
311 info!(
312 "Building deposit proof: value={}",
313 deposit_result.note.value
314 );
315
316 let inputs = DepositInputs {
317 note_plaintext: deposit_result.note.clone(),
318 ephemeral_sk: deposit_result.ephemeral_sk,
319 compliance_pk: self.config.compliance_pk,
320 };
321
322 let proof_data = self
323 .prover
324 .prove_deposit(&inputs)
325 .await
326 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
327
328 let public_inputs = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
329
330 let calldata = encode_deposit_calldata(&proof_data.proof, &public_inputs);
331
332 info!("Deposit proof generated: {} bytes", proof_data.proof.len());
333
334 Ok(DepositProofBundle {
335 proof: proof_data.proof,
336 public_inputs,
337 deposit: deposit_result.clone(),
338 calldata,
339 })
340 }
341
342 #[allow(clippy::too_many_arguments)]
343 pub async fn build_withdraw(
344 &self,
345 spending_inputs: &SpendingInputs,
346 withdraw_value: U256,
347 recipient: Address,
348 merkle_root: U256,
349 change_note: Option<&ChangeNoteResult>,
350 intent_hash: Option<U256>,
351 current_timestamp: u64,
352 ) -> Result<WithdrawProofBundle, BuilderError> {
353 info!(
354 "Building withdraw proof: value={}, recipient={:?}",
355 withdraw_value, recipient
356 );
357
358 let change = if let Some(cn) = change_note {
359 cn.note.clone()
360 } else {
361 NotePlaintext::random(U256::zero(), Address::zero())
362 };
363 let change_sk = change_note.map_or_else(generate_random_scalar, |c| c.ephemeral_sk);
364
365 let inputs = WithdrawInputs {
366 withdraw_value,
367 recipient,
368 merkle_root,
369 current_timestamp,
370 intent_hash: intent_hash.unwrap_or(U256::zero()),
371 compliance_pk: self.config.compliance_pk,
372 old_note: spending_inputs.note.clone(),
373 old_shared_secret: spending_inputs.shared_secret,
374 old_note_index: spending_inputs.leaf_index,
375 old_note_path: spending_inputs.merkle_path.siblings_vec(),
376 hashlock_preimage: spending_inputs.hashlock_preimage,
377 change_note: change,
378 change_ephemeral_sk: change_sk,
379 };
380
381 let proof_data = self
382 .prover
383 .prove_withdraw(&inputs)
384 .await
385 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
386
387 let public_inputs = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
388 let nullifier_hash = extract_withdraw_nullifier(&public_inputs)?;
389
390 let calldata = encode_withdraw_calldata(&proof_data.proof, &public_inputs);
391
392 info!("Withdraw proof generated: {} bytes", proof_data.proof.len());
393
394 Ok(WithdrawProofBundle {
395 proof: proof_data.proof,
396 public_inputs,
397 nullifier_hash,
398 change_note: change_note.cloned(),
399 calldata,
400 })
401 }
402
403 #[allow(clippy::too_many_arguments)]
404 pub async fn build_transfer(
405 &self,
406 spending_inputs: &SpendingInputs,
407 merkle_root: U256,
408 recipient_b: (U256, U256),
409 recipient_p: (U256, U256),
410 recipient_proof: DLEQProof,
411 memo_note: NotePlaintext,
412 memo_ephemeral_sk: U256,
413 change_note: NotePlaintext,
414 change_ephemeral_sk: U256,
415 current_timestamp: u64,
416 ) -> Result<TransferProofBundle, BuilderError> {
417 info!("Building transfer proof: memo_value={}", memo_note.value);
418
419 let inputs = TransferInputs {
420 merkle_root,
421 current_timestamp,
422 compliance_pk: self.config.compliance_pk,
423 recipient_b,
424 recipient_p,
425 recipient_proof,
426 old_note: spending_inputs.note.clone(),
427 old_shared_secret: spending_inputs.shared_secret,
428 old_note_index: spending_inputs.leaf_index,
429 old_note_path: spending_inputs.merkle_path.siblings_vec(),
430 hashlock_preimage: spending_inputs.hashlock_preimage,
431 memo_note,
432 memo_ephemeral_sk,
433 change_note,
434 change_ephemeral_sk,
435 };
436
437 let proof_data = self
438 .prover
439 .prove_transfer(&inputs)
440 .await
441 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
442
443 let public_inputs = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
444 let nullifier_hash = extract_transfer_nullifier(&public_inputs)?;
445
446 let calldata = encode_transfer_calldata(&proof_data.proof, &public_inputs);
447 let (memo_commitment, change_commitment, transfer_tag) =
448 extract_transfer_commitments(&proof_data.public_inputs)?;
449
450 info!("Transfer proof generated: {} bytes", proof_data.proof.len());
451
452 Ok(TransferProofBundle {
453 proof: proof_data.proof,
454 public_inputs,
455 nullifier_hash,
456 memo_commitment,
457 change_commitment,
458 transfer_tag,
459 calldata,
460 })
461 }
462
463 pub async fn build_split(
464 &self,
465 spending_inputs: &SpendingInputs,
466 merkle_root: U256,
467 note_out_1: &ChangeNoteResult,
468 note_out_2: &ChangeNoteResult,
469 current_timestamp: u64,
470 ) -> Result<SplitProofBundle, BuilderError> {
471 info!(
472 "Building split proof: value_1={}, value_2={}",
473 note_out_1.note.value, note_out_2.note.value
474 );
475
476 let inputs = SplitInputs {
477 merkle_root,
478 current_timestamp,
479 compliance_pk: self.config.compliance_pk,
480 note_in: spending_inputs.note.clone(),
481 secret_in: spending_inputs.shared_secret,
482 index_in: spending_inputs.leaf_index,
483 path_in: spending_inputs.merkle_path.siblings_vec(),
484 preimage_in: spending_inputs.hashlock_preimage,
485 note_out_1: note_out_1.note.clone(),
486 sk_out_1: note_out_1.ephemeral_sk,
487 note_out_2: note_out_2.note.clone(),
488 sk_out_2: note_out_2.ephemeral_sk,
489 };
490
491 let proof_data = self
492 .prover
493 .prove_split(&inputs)
494 .await
495 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
496
497 let public_inputs = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
498 let nullifier_hash = extract_split_nullifier(&public_inputs)?;
499
500 let calldata = encode_split_calldata(&proof_data.proof, &public_inputs);
501
502 info!("Split proof generated: {} bytes", proof_data.proof.len());
503
504 Ok(SplitProofBundle {
505 proof: proof_data.proof,
506 public_inputs,
507 nullifier_hash,
508 note_out_1: note_out_1.clone(),
509 note_out_2: note_out_2.clone(),
510 calldata,
511 })
512 }
513
514 pub async fn build_join(
515 &self,
516 inputs_a: &SpendingInputs,
517 inputs_b: &SpendingInputs,
518 merkle_root: U256,
519 note_out: &ChangeNoteResult,
520 current_timestamp: u64,
521 ) -> Result<JoinProofBundle, BuilderError> {
522 info!(
523 "Building join proof: value_a={} + value_b={} = {}",
524 inputs_a.note.value, inputs_b.note.value, note_out.note.value
525 );
526
527 let inputs = JoinInputs {
528 merkle_root,
529 current_timestamp,
530 compliance_pk: self.config.compliance_pk,
531 note_a: inputs_a.note.clone(),
532 secret_a: inputs_a.shared_secret,
533 index_a: inputs_a.leaf_index,
534 path_a: inputs_a.merkle_path.siblings_vec(),
535 preimage_a: inputs_a.hashlock_preimage,
536 note_b: inputs_b.note.clone(),
537 secret_b: inputs_b.shared_secret,
538 index_b: inputs_b.leaf_index,
539 path_b: inputs_b.merkle_path.siblings_vec(),
540 preimage_b: inputs_b.hashlock_preimage,
541 note_out: note_out.note.clone(),
542 sk_out: note_out.ephemeral_sk,
543 };
544
545 let proof_data = self
546 .prover
547 .prove_join(&inputs)
548 .await
549 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
550
551 let public_inputs = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
552
553 let (nullifier_hash_a, nullifier_hash_b) = extract_join_nullifiers(&public_inputs)?;
554
555 let calldata = encode_join_calldata(&proof_data.proof, &public_inputs);
556
557 info!("Join proof generated: {} bytes", proof_data.proof.len());
558
559 Ok(JoinProofBundle {
560 proof: proof_data.proof,
561 public_inputs,
562 nullifier_hash_a,
563 nullifier_hash_b,
564 note_out: note_out.clone(),
565 calldata,
566 })
567 }
568
569 #[allow(clippy::too_many_arguments)]
570 pub async fn build_public_claim(
571 &self,
572 memo_id: U256,
573 val: U256,
574 asset_id: U256,
575 timelock: U256,
576 owner_pk: (U256, U256),
577 salt: U256,
578 recipient_sk: U256,
579 note_out: &ChangeNoteResult,
580 ) -> Result<PublicClaimProofBundle, BuilderError> {
581 info!("Building public claim proof: memo_id={}", memo_id);
582
583 let inputs = PublicClaimInputs {
584 memo_id,
585 compliance_pk: self.config.compliance_pk,
586 val,
587 asset_id,
588 timelock,
589 owner_x: owner_pk.0,
590 owner_y: owner_pk.1,
591 salt,
592 recipient_sk,
593 note_out: note_out.note.clone(),
594 sk_out: note_out.ephemeral_sk,
595 };
596
597 let proof_data = self
598 .prover
599 .prove_public_claim(&inputs)
600 .await
601 .map_err(|e| BuilderError::ProofGeneration(e.to_string()))?;
602
603 let public_inputs = convert_public_inputs_to_bytes32(&proof_data.public_inputs)?;
604 let calldata = encode_public_claim_calldata(&proof_data.proof, &public_inputs);
605
606 info!(
607 "Public claim proof generated: {} bytes",
608 proof_data.proof.len()
609 );
610
611 Ok(PublicClaimProofBundle {
612 proof: proof_data.proof,
613 public_inputs,
614 note_out: note_out.clone(),
615 calldata,
616 })
617 }
618}
619
620pub fn convert_public_inputs_to_bytes32(inputs: &[String]) -> Result<Vec<[u8; 32]>, BuilderError> {
622 inputs
623 .iter()
624 .map(|input| {
625 let hex_str = input.trim_start_matches("0x");
626 let bytes = hex::decode(hex_str)
627 .map_err(|e| BuilderError::Encoding(format!("Invalid hex: {e}")))?;
628
629 if bytes.len() != 32 {
630 return Err(BuilderError::Encoding(format!(
631 "Public input must be 32 bytes, got {}",
632 bytes.len()
633 )));
634 }
635
636 let mut arr = [0u8; 32];
637 arr.copy_from_slice(&bytes);
638 Ok(arr)
639 })
640 .collect()
641}
642
643pub fn compute_execution_hash(target: &Address, calldata: &Bytes, fee: &U256) -> H256 {
646 use ethers::utils::keccak256;
647
648 let mut data = Vec::new();
649 data.extend_from_slice(target.as_bytes());
650 data.extend_from_slice(calldata.as_ref());
651 let mut fee_bytes = [0u8; 32];
652 fee.to_big_endian(&mut fee_bytes);
653 data.extend_from_slice(&fee_bytes);
654
655 let hash = keccak256(&data);
656
657 let hash_u256 = U256::from_big_endian(&hash);
658 let reduced = hash_u256 % *BN254_MODULUS;
659
660 let mut result_bytes = [0u8; 32];
661 reduced.to_big_endian(&mut result_bytes);
662 H256::from_slice(&result_bytes)
663}
664
665fn generate_random_scalar() -> U256 {
668 darkpool_crypto::random_bjj_scalar()
669}
670
671pub fn encode_multicall(
673 darkpool: Address,
674 proof: &[u8],
675 public_inputs: &[[u8; 32]],
676 action_target: Address,
677 action_calldata: Bytes,
678) -> Result<Bytes, BuilderError> {
679 let proof_token = Token::Bytes(proof.to_vec());
681 let inputs_token = Token::Array(
682 public_inputs
683 .iter()
684 .map(|b| Token::FixedBytes(b.to_vec()))
685 .collect(),
686 );
687
688 let pay_relayer_selector: [u8; 4] = [0x24, 0xfc, 0xf1, 0x31];
689 let mut darkpool_calldata = pay_relayer_selector.to_vec();
690 darkpool_calldata.extend(encode(&[proof_token, inputs_token]));
691
692 let calls = vec![
693 Token::Tuple(vec![
694 Token::Address(darkpool),
695 Token::Bytes(darkpool_calldata),
696 Token::Uint(U256::zero()),
697 Token::Bool(true),
698 ]),
699 Token::Tuple(vec![
700 Token::Address(action_target),
701 Token::Bytes(action_calldata.to_vec()),
702 Token::Uint(U256::zero()),
703 Token::Bool(true),
704 ]),
705 ];
706
707 let mut encoded = vec![0xcf, 0xfb, 0x5c, 0xd6];
709 encoded.extend(encode(&[Token::Array(calls)]));
710
711 Ok(Bytes::from(encoded))
712}
713
714#[must_use]
716pub fn format_proof_for_solidity(proof: &[u8]) -> Bytes {
717 Bytes::from(proof.to_vec())
718}
719
720pub fn format_public_inputs_for_solidity(inputs: &[String]) -> Result<Vec<[u8; 32]>, BuilderError> {
722 convert_public_inputs_to_bytes32(inputs)
723}
724
725fn extract_withdraw_nullifier(public_inputs: &[[u8; 32]]) -> Result<H256, BuilderError> {
727 public_inputs
728 .get(7)
729 .map(|b| H256::from_slice(b))
730 .ok_or_else(|| {
731 BuilderError::Encoding(format!(
732 "Withdraw public inputs too short: expected index 7, got length {}",
733 public_inputs.len()
734 ))
735 })
736}
737
738fn extract_transfer_nullifier(public_inputs: &[[u8; 32]]) -> Result<H256, BuilderError> {
740 public_inputs
741 .get(8)
742 .map(|b| H256::from_slice(b))
743 .ok_or_else(|| {
744 BuilderError::Encoding(format!(
745 "Transfer public inputs too short: expected index 8, got length {}",
746 public_inputs.len()
747 ))
748 })
749}
750
751fn extract_join_nullifiers(public_inputs: &[[u8; 32]]) -> Result<(H256, H256), BuilderError> {
753 let a = public_inputs
754 .get(4)
755 .map(|b| H256::from_slice(b))
756 .ok_or_else(|| {
757 BuilderError::Encoding(format!(
758 "Join public inputs too short: expected index 4, got length {}",
759 public_inputs.len()
760 ))
761 })?;
762 let b = public_inputs
763 .get(5)
764 .map(|b| H256::from_slice(b))
765 .ok_or_else(|| {
766 BuilderError::Encoding(format!(
767 "Join public inputs too short: expected index 5, got length {}",
768 public_inputs.len()
769 ))
770 })?;
771 Ok((a, b))
772}
773
774fn extract_split_nullifier(public_inputs: &[[u8; 32]]) -> Result<H256, BuilderError> {
776 public_inputs
777 .get(4)
778 .map(|b| H256::from_slice(b))
779 .ok_or_else(|| {
780 BuilderError::Encoding(format!(
781 "Split public inputs too short: expected index 4, got length {}",
782 public_inputs.len()
783 ))
784 })
785}
786
787fn extract_gas_payment_nullifier(public_inputs: &[[u8; 32]]) -> Result<H256, BuilderError> {
789 public_inputs
790 .get(8)
791 .map(|b| H256::from_slice(b))
792 .ok_or_else(|| {
793 BuilderError::Encoding(format!(
794 "Gas payment public inputs too short: expected index 8, got length {}",
795 public_inputs.len()
796 ))
797 })
798}
799
800fn extract_transfer_commitments(
805 public_inputs_hex: &[String],
806) -> Result<(U256, U256, U256), BuilderError> {
807 use crate::crypto_helpers::poseidon_hash;
808
809 if public_inputs_hex.len() < 31 {
810 return Err(BuilderError::Encoding(format!(
811 "Transfer proof needs at least 31 public inputs, got {}",
812 public_inputs_hex.len()
813 )));
814 }
815
816 let parse_hex = |idx: usize| -> Result<U256, BuilderError> {
817 let hex_str = public_inputs_hex.get(idx).ok_or_else(|| {
818 BuilderError::Encoding(format!("Missing public input at index {idx}"))
819 })?;
820 let clean = hex_str.trim_start_matches("0x");
821 let padded = format!("{clean:0>64}");
822 let bytes = hex::decode(&padded)
823 .map_err(|e| BuilderError::Encoding(format!("Invalid hex at {idx}: {e}")))?;
824 Ok(U256::from_big_endian(&bytes))
825 };
826
827 let memo_packed: Vec<U256> = (11..=17).map(parse_hex).collect::<Result<_, _>>()?;
828 let change_packed: Vec<U256> = (24..=30).map(parse_hex).collect::<Result<_, _>>()?;
829
830 let memo_commitment = poseidon_hash(&memo_packed);
831 let change_commitment = poseidon_hash(&change_packed);
832 let transfer_tag = parse_hex(18)?;
833
834 Ok((memo_commitment, change_commitment, transfer_tag))
835}
836
837fn encode_calldata(fn_sig: &[u8], proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
840 use ethers::utils::keccak256;
841
842 let hash = keccak256(fn_sig);
843 let selector = [hash[0], hash[1], hash[2], hash[3]];
844
845 let proof_token = Token::Bytes(proof.to_vec());
846 let inputs_token = Token::Array(
847 public_inputs
848 .iter()
849 .map(|b| Token::FixedBytes(b.to_vec()))
850 .collect(),
851 );
852
853 let mut calldata = selector.to_vec();
854 calldata.extend(encode(&[proof_token, inputs_token]));
855
856 Bytes::from(calldata)
857}
858
859fn encode_deposit_calldata(proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
860 encode_calldata(b"deposit(bytes,bytes32[])", proof, public_inputs)
861}
862
863fn encode_withdraw_calldata(proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
864 encode_calldata(b"withdraw(bytes,bytes32[])", proof, public_inputs)
865}
866
867fn encode_transfer_calldata(proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
868 encode_calldata(b"privateTransfer(bytes,bytes32[])", proof, public_inputs)
869}
870
871fn encode_split_calldata(proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
872 encode_calldata(b"split(bytes,bytes32[])", proof, public_inputs)
873}
874
875fn encode_join_calldata(proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
876 encode_calldata(b"join(bytes,bytes32[])", proof, public_inputs)
877}
878
879fn encode_public_claim_calldata(proof: &[u8], public_inputs: &[[u8; 32]]) -> Bytes {
880 encode_calldata(b"publicClaim(bytes,bytes32[])", proof, public_inputs)
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886
887 #[test]
888 fn test_execution_hash() {
889 let target = Address::zero();
890 let calldata = Bytes::from(vec![1, 2, 3, 4]);
891 let fee = U256::from(1000);
892
893 let hash = compute_execution_hash(&target, &calldata, &fee);
894
895 let hash2 = compute_execution_hash(&target, &calldata, &fee);
896 assert_eq!(hash, hash2);
897
898 let hash3 = compute_execution_hash(&target, &calldata, &U256::from(1001));
899 assert_ne!(hash, hash3);
900 }
901
902 #[test]
904 fn test_keccak256_bn254_reduction() {
905 let bn254_modulus = *BN254_MODULUS;
906
907 for i in 0u64..50 {
908 let target = Address::from_slice(&{
909 let mut b = [0u8; 20];
910 b[..8].copy_from_slice(&i.to_le_bytes());
911 b
912 });
913 let calldata = Bytes::from(i.to_le_bytes().to_vec());
914 let fee = U256::from(i * 1_000_000u64);
915
916 let hash = compute_execution_hash(&target, &calldata, &fee);
917 let hash_as_u256 = U256::from_big_endian(hash.as_bytes());
918
919 assert!(
920 hash_as_u256 < bn254_modulus,
921 "compute_execution_hash output {hash_as_u256} >= BN254_MODULUS for input i={i}"
922 );
923 }
924 }
925
926 #[test]
927 fn test_convert_public_inputs() {
928 let inputs = vec![
929 "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(),
930 "0x0000000000000000000000000000000000000000000000000000000000000002".to_string(),
931 ];
932
933 let result = convert_public_inputs_to_bytes32(&inputs).unwrap();
934
935 assert_eq!(result.len(), 2);
936 assert_eq!(result[0][31], 1);
937 assert_eq!(result[1][31], 2);
938 }
939
940 #[test]
941 fn test_encode_multicall() {
942 let darkpool = Address::random();
943 let proof = vec![0u8; 100];
944 let public_inputs = vec![[0u8; 32]; 5];
945 let action_target = Address::random();
946 let action_calldata = Bytes::from(vec![1, 2, 3, 4]);
947
948 let result = encode_multicall(
949 darkpool,
950 &proof,
951 &public_inputs,
952 action_target,
953 action_calldata,
954 );
955
956 assert!(result.is_ok());
957 let encoded = result.unwrap();
958
959 assert_eq!(&encoded[0..4], &[0xcf, 0xfb, 0x5c, 0xd6]);
960 }
961}