|
11 | 11 | //! For in-memory data uploads, see the `data` module. |
12 | 12 |
|
13 | 13 | 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 | +}; |
15 | 18 | use crate::data::client::Client; |
16 | 19 | use crate::data::error::{Error, Result}; |
17 | 20 | use ant_node::ant_protocol::DATA_TYPE_CHUNK; |
@@ -340,25 +343,42 @@ pub struct FileUploadResult { |
340 | 343 | pub payment_mode_used: PaymentMode, |
341 | 344 | } |
342 | 345 |
|
| 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 | + |
343 | 367 | /// Prepared upload ready for external payment. |
344 | 368 | /// |
345 | 369 | /// Contains everything needed to construct the on-chain payment transaction |
346 | 370 | /// externally (e.g. via WalletConnect in a desktop app) and then finalize |
347 | 371 | /// the upload without a Rust-side wallet. |
348 | 372 | /// |
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`. |
352 | 376 | #[derive(Debug)] |
353 | 377 | pub struct PreparedUpload { |
354 | 378 | /// The data map for later retrieval. |
355 | 379 | 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, |
362 | 382 | } |
363 | 383 |
|
364 | 384 | /// Return type for [`spawn_file_encryption`]: chunk receiver, `DataMap` oneshot, join handle. |
@@ -507,72 +527,163 @@ impl Client { |
507 | 527 | .map(|addr| spill.read_chunk(addr)) |
508 | 528 | .collect::<std::result::Result<Vec<_>, _>>()?; |
509 | 529 |
|
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 | + } |
521 | 572 | } |
522 | | - } |
523 | 573 |
|
524 | | - let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks); |
| 574 | + let payment_intent = PaymentIntent::from_prepared_chunks(&prepared_chunks); |
525 | 575 |
|
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 | + }; |
532 | 588 |
|
533 | 589 | Ok(PreparedUpload { |
534 | 590 | data_map, |
535 | | - prepared_chunks, |
536 | | - payment_intent, |
537 | | - payment_mode: PaymentMode::Single, |
| 591 | + payment_info, |
538 | 592 | }) |
539 | 593 | } |
540 | 594 |
|
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. |
542 | 596 | /// |
543 | | - /// Takes a [`PreparedUpload`] from [`Client::file_prepare_upload`] and a map |
| 597 | + /// Takes a [`PreparedUpload`] that used wave-batch payment and a map |
544 | 598 | /// of `quote_hash -> tx_hash` provided by the external signer after on-chain |
545 | 599 | /// payment. Builds payment proofs and stores chunks on the network. |
546 | 600 | /// |
547 | 601 | /// # Errors |
548 | 602 | /// |
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. |
550 | 606 | pub async fn finalize_upload( |
551 | 607 | &self, |
552 | 608 | prepared: PreparedUpload, |
553 | 609 | tx_hash_map: &HashMap<QuoteHash, TxHash>, |
554 | 610 | ) -> 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(); |
568 | 629 |
|
569 | | - info!("External-signer upload finalized: {chunks_stored} chunks stored"); |
| 630 | + info!("External-signer upload finalized: {chunks_stored} chunks stored"); |
570 | 631 |
|
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 | + } |
576 | 687 | } |
577 | 688 |
|
578 | 689 | /// Upload a file with a specific payment mode. |
|
0 commit comments