Skip to content

Commit 1d9348d

Browse files
authored
Merge pull request #12 from WithAutonomi/feat-merkle-external-signer
feat: merkle batch payment external signer support
2 parents 51e74d4 + bd730f4 commit 1d9348d

4 files changed

Lines changed: 404 additions & 106 deletions

File tree

ant-core/src/data/client/data.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! file into memory, see the `file` module.
88
99
use crate::data::client::batch::{PaymentIntent, PreparedChunk};
10-
use crate::data::client::file::PreparedUpload;
10+
use crate::data::client::file::{ExternalPaymentInfo, PreparedUpload};
1111
use crate::data::client::merkle::PaymentMode;
1212
use crate::data::client::Client;
1313
use crate::data::error::{Error, Result};
@@ -201,9 +201,10 @@ impl Client {
201201

202202
Ok(PreparedUpload {
203203
data_map,
204-
prepared_chunks,
205-
payment_intent,
206-
payment_mode: PaymentMode::Single,
204+
payment_info: ExternalPaymentInfo::WaveBatch {
205+
prepared_chunks,
206+
payment_intent,
207+
},
207208
})
208209
}
209210

ant-core/src/data/client/file.rs

Lines changed: 165 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
//! For in-memory data uploads, see the `data` module.
1212
1313
use crate::data::client::batch::{finalize_batch_payment, PaymentIntent, PreparedChunk};
14-
use crate::data::client::merkle::{MerkleBatchPaymentResult, PaymentMode};
14+
use crate::data::client::merkle::{
15+
finalize_merkle_batch, should_use_merkle, MerkleBatchPaymentResult, PaymentMode,
16+
PreparedMerkleBatch,
17+
};
1518
use crate::data::client::Client;
1619
use crate::data::error::{Error, Result};
1720
use ant_node::ant_protocol::DATA_TYPE_CHUNK;
@@ -340,25 +343,42 @@ pub struct FileUploadResult {
340343
pub payment_mode_used: PaymentMode,
341344
}
342345

346+
/// Payment information for external signing — either wave-batch or merkle.
347+
#[derive(Debug)]
348+
pub enum ExternalPaymentInfo {
349+
/// Wave-batch: individual (quote_hash, rewards_address, amount) tuples.
350+
WaveBatch {
351+
/// Chunks ready for payment (needed for finalize).
352+
prepared_chunks: Vec<PreparedChunk>,
353+
/// Payment intent for external signing.
354+
payment_intent: PaymentIntent,
355+
},
356+
/// Merkle: single on-chain call with depth, pool commitments, timestamp.
357+
Merkle {
358+
/// The prepared merkle batch (public fields sent to frontend, private fields stay in Rust).
359+
prepared_batch: PreparedMerkleBatch,
360+
/// Raw chunk contents (needed for upload after payment).
361+
chunk_contents: Vec<Bytes>,
362+
/// Chunk addresses in order (needed for upload after payment).
363+
chunk_addresses: Vec<[u8; 32]>,
364+
},
365+
}
366+
343367
/// Prepared upload ready for external payment.
344368
///
345369
/// Contains everything needed to construct the on-chain payment transaction
346370
/// externally (e.g. via WalletConnect in a desktop app) and then finalize
347371
/// the upload without a Rust-side wallet.
348372
///
349-
/// Note: This struct stays in Rust memory — only `payment_intent` is sent
350-
/// to the frontend. `PreparedChunk` contains non-serializable network types,
351-
/// so the full struct cannot derive `Serialize`.
373+
/// Note: This struct stays in Rust memory — only the public fields of
374+
/// `payment_info` are sent to the frontend. `PreparedChunk` contains
375+
/// non-serializable network types, so the full struct cannot derive `Serialize`.
352376
#[derive(Debug)]
353377
pub struct PreparedUpload {
354378
/// The data map for later retrieval.
355379
pub data_map: DataMap,
356-
/// Chunks ready for payment.
357-
pub prepared_chunks: Vec<PreparedChunk>,
358-
/// Payment intent for external signing.
359-
pub payment_intent: PaymentIntent,
360-
/// The payment mode used for this upload.
361-
pub payment_mode: PaymentMode,
380+
/// Payment information — either wave-batch or merkle depending on chunk count.
381+
pub payment_info: ExternalPaymentInfo,
362382
}
363383

364384
/// Return type for [`spawn_file_encryption`]: chunk receiver, `DataMap` oneshot, join handle.
@@ -507,72 +527,163 @@ impl Client {
507527
.map(|addr| spill.read_chunk(addr))
508528
.collect::<std::result::Result<Vec<_>, _>>()?;
509529

510-
let concurrency = self.config().chunk_concurrency;
511-
let results: Vec<Result<Option<PreparedChunk>>> = stream::iter(chunk_data)
512-
.map(|content| async move { self.prepare_chunk_payment(content).await })
513-
.buffer_unordered(concurrency)
514-
.collect()
515-
.await;
516-
517-
let mut prepared_chunks = Vec::with_capacity(spill.len());
518-
for result in results {
519-
if let Some(prepared) = result? {
520-
prepared_chunks.push(prepared);
530+
let chunk_count = chunk_data.len();
531+
532+
let payment_info = if should_use_merkle(chunk_count, PaymentMode::Auto) {
533+
// Merkle path: build tree, collect candidate pools, return for external payment.
534+
info!("Using merkle batch preparation for {chunk_count} file chunks");
535+
536+
let addresses: Vec<[u8; 32]> = chunk_data.iter().map(|c| compute_address(c)).collect();
537+
538+
let avg_size =
539+
chunk_data.iter().map(bytes::Bytes::len).sum::<usize>() / chunk_count.max(1);
540+
let avg_size_u64 = u64::try_from(avg_size).unwrap_or(0);
541+
542+
let prepared_batch = self
543+
.prepare_merkle_batch_external(&addresses, DATA_TYPE_CHUNK, avg_size_u64)
544+
.await?;
545+
546+
info!(
547+
"File prepared for external merkle signing: {} chunks, depth={} ({})",
548+
chunk_count,
549+
prepared_batch.depth,
550+
path.display()
551+
);
552+
553+
ExternalPaymentInfo::Merkle {
554+
prepared_batch,
555+
chunk_contents: chunk_data,
556+
chunk_addresses: addresses,
557+
}
558+
} else {
559+
// Wave-batch path: collect quotes per chunk concurrently.
560+
let concurrency = self.config().chunk_concurrency;
561+
let results: Vec<Result<Option<PreparedChunk>>> = stream::iter(chunk_data)
562+
.map(|content| async move { self.prepare_chunk_payment(content).await })
563+
.buffer_unordered(concurrency)
564+
.collect()
565+
.await;
566+
567+
let mut prepared_chunks = Vec::with_capacity(spill.len());
568+
for result in results {
569+
if let Some(prepared) = result? {
570+
prepared_chunks.push(prepared);
571+
}
521572
}
522-
}
523573

524-
let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks);
574+
let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks);
525575

526-
info!(
527-
"File prepared for external signing: {} chunks, total {} atto ({})",
528-
prepared_chunks.len(),
529-
payment_intent.total_amount,
530-
path.display()
531-
);
576+
info!(
577+
"File prepared for external signing: {} chunks, total {} atto ({})",
578+
prepared_chunks.len(),
579+
payment_intent.total_amount,
580+
path.display()
581+
);
582+
583+
ExternalPaymentInfo::WaveBatch {
584+
prepared_chunks,
585+
payment_intent,
586+
}
587+
};
532588

533589
Ok(PreparedUpload {
534590
data_map,
535-
prepared_chunks,
536-
payment_intent,
537-
payment_mode: PaymentMode::Single,
591+
payment_info,
538592
})
539593
}
540594

541-
/// Phase 2 of external-signer upload: finalize with externally-signed tx hashes.
595+
/// Phase 2 of external-signer upload (wave-batch): finalize with externally-signed tx hashes.
542596
///
543-
/// Takes a [`PreparedUpload`] from [`Client::file_prepare_upload`] and a map
597+
/// Takes a [`PreparedUpload`] that used wave-batch payment and a map
544598
/// of `quote_hash -> tx_hash` provided by the external signer after on-chain
545599
/// payment. Builds payment proofs and stores chunks on the network.
546600
///
547601
/// # Errors
548602
///
549-
/// Returns an error if proof construction fails or any chunk cannot be stored.
603+
/// Returns an error if the prepared upload used merkle payment (use
604+
/// [`Client::finalize_upload_merkle`] instead), proof construction fails,
605+
/// or any chunk cannot be stored.
550606
pub async fn finalize_upload(
551607
&self,
552608
prepared: PreparedUpload,
553609
tx_hash_map: &HashMap<QuoteHash, TxHash>,
554610
) -> Result<FileUploadResult> {
555-
let paid_chunks = finalize_batch_payment(prepared.prepared_chunks, tx_hash_map)?;
556-
let wave_result = self.store_paid_chunks(paid_chunks).await;
557-
if !wave_result.failed.is_empty() {
558-
let failed_count = wave_result.failed.len();
559-
return Err(Error::PartialUpload {
560-
stored: wave_result.stored.clone(),
561-
stored_count: wave_result.stored.len(),
562-
failed: wave_result.failed,
563-
failed_count,
564-
reason: "finalize_upload: chunk storage failed after retries".into(),
565-
});
566-
}
567-
let chunks_stored = wave_result.stored.len();
611+
match prepared.payment_info {
612+
ExternalPaymentInfo::WaveBatch {
613+
prepared_chunks,
614+
payment_intent: _,
615+
} => {
616+
let paid_chunks = finalize_batch_payment(prepared_chunks, tx_hash_map)?;
617+
let wave_result = self.store_paid_chunks(paid_chunks).await;
618+
if !wave_result.failed.is_empty() {
619+
let failed_count = wave_result.failed.len();
620+
return Err(Error::PartialUpload {
621+
stored: wave_result.stored.clone(),
622+
stored_count: wave_result.stored.len(),
623+
failed: wave_result.failed,
624+
failed_count,
625+
reason: "finalize_upload: chunk storage failed after retries".into(),
626+
});
627+
}
628+
let chunks_stored = wave_result.stored.len();
568629

569-
info!("External-signer upload finalized: {chunks_stored} chunks stored");
630+
info!("External-signer upload finalized: {chunks_stored} chunks stored");
570631

571-
Ok(FileUploadResult {
572-
data_map: prepared.data_map,
573-
chunks_stored,
574-
payment_mode_used: prepared.payment_mode,
575-
})
632+
Ok(FileUploadResult {
633+
data_map: prepared.data_map,
634+
chunks_stored,
635+
payment_mode_used: PaymentMode::Single,
636+
})
637+
}
638+
ExternalPaymentInfo::Merkle { .. } => Err(Error::Payment(
639+
"Cannot finalize merkle upload with wave-batch tx hashes. \
640+
Use finalize_upload_merkle() instead."
641+
.to_string(),
642+
)),
643+
}
644+
}
645+
646+
/// Phase 2 of external-signer upload (merkle): finalize with winner pool hash.
647+
///
648+
/// Takes a [`PreparedUpload`] that used merkle payment and the `winner_pool_hash`
649+
/// returned by the on-chain merkle payment transaction. Generates proofs and
650+
/// stores chunks on the network.
651+
///
652+
/// # Errors
653+
///
654+
/// Returns an error if the prepared upload used wave-batch payment (use
655+
/// [`Client::finalize_upload`] instead), proof generation fails,
656+
/// or any chunk cannot be stored.
657+
pub async fn finalize_upload_merkle(
658+
&self,
659+
prepared: PreparedUpload,
660+
winner_pool_hash: [u8; 32],
661+
) -> Result<FileUploadResult> {
662+
match prepared.payment_info {
663+
ExternalPaymentInfo::Merkle {
664+
prepared_batch,
665+
chunk_contents,
666+
chunk_addresses,
667+
} => {
668+
let batch_result = finalize_merkle_batch(prepared_batch, winner_pool_hash)?;
669+
let chunks_stored = self
670+
.merkle_upload_chunks(chunk_contents, chunk_addresses, &batch_result)
671+
.await?;
672+
673+
info!("External-signer merkle upload finalized: {chunks_stored} chunks stored");
674+
675+
Ok(FileUploadResult {
676+
data_map: prepared.data_map,
677+
chunks_stored,
678+
payment_mode_used: PaymentMode::Merkle,
679+
})
680+
}
681+
ExternalPaymentInfo::WaveBatch { .. } => Err(Error::Payment(
682+
"Cannot finalize wave-batch upload with merkle winner hash. \
683+
Use finalize_upload() instead."
684+
.to_string(),
685+
)),
686+
}
576687
}
577688

578689
/// Upload a file with a specific payment mode.

0 commit comments

Comments
 (0)