From 5119e87a00dc2e3f44917dcd7000d3de2127364e Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 27 May 2026 18:33:12 +0700 Subject: [PATCH 01/44] feat: fix create_bucket_with_storage extrinsic `create_bucket_with_storage` takes an explicit provider account; then the pallet then performs an O(1) lookup of that single provider and re-validates all constraints before opening the agreement --- pallet/src/benchmarking.rs | 3 +- pallet/src/lib.rs | 178 ++++++++++++------------------------- pallet/src/tests.rs | 62 +++++++------ 3 files changed, 98 insertions(+), 145 deletions(-) diff --git a/pallet/src/benchmarking.rs b/pallet/src/benchmarking.rs index 8bafd185..f715e524 100644 --- a/pallet/src/benchmarking.rs +++ b/pallet/src/benchmarking.rs @@ -281,7 +281,7 @@ mod benchmarks { #[benchmark] fn create_bucket_with_storage() { // Create a provider first - let _provider = create_provider::(0); + let provider = create_provider::(0); let admin = funded_account::("admin", 1); let max_bytes = 1_000u64; @@ -291,6 +291,7 @@ mod benchmarks { #[extrinsic_call] create_bucket_with_storage( RawOrigin::Signed(admin), + provider, max_bytes, duration, max_price_per_byte, diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 3fed6249..ec07032d 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -44,7 +44,7 @@ pub mod pallet { }; use frame_system::pallet_prelude::*; use sp_core::H256; - use sp_runtime::traits::{Bounded, CheckedAdd, Saturating, Zero}; + use sp_runtime::traits::{Bounded, CheckedAdd, SaturatedConversion, Saturating, Zero}; use storage_primitives::{ BucketId, BucketSnapshot, ChallengeId, CommitmentPayload, EndAction, MerkleProof, MmrProof, ProviderRole, RemovalReason, ReplicaRequestParams, Role, HISTORICAL_ROOT_PRIMES, @@ -741,8 +741,7 @@ pub mod pallet { CapacityExceeded, /// Stake insufficient to back declared capacity. InsufficientStakeForCapacity, - /// Provider settings specify `min_duration > max_duration`, which - /// would silently brick the provider in `find_matching_provider`. + /// Provider settings specify `min_duration > max_duration MinDurationExceedsMaxDuration, /// Provider has already announced a deregistration; the action is /// rejected until they complete or cancel it. @@ -835,9 +834,9 @@ pub mod pallet { /// Account is a member of too many buckets. TooManyBucketsForMember, - // Auto-matching errors - /// No provider found matching the storage requirements. - NoMatchingProvider, + /// The selected provider's price per byte exceeds the caller's + /// `max_price_per_byte`. + PriceExceedsMax, } // ───────────────────────────────────────────────────────────────────────── @@ -1110,7 +1109,6 @@ pub mod pallet { ); // Validate stake backs declared capacity - use sp_runtime::traits::SaturatedConversion; let capacity_as_balance: BalanceOf = settings.max_capacity.saturated_into(); let required_stake = T::MinStakePerByte::get() .checked_mul(&capacity_as_balance) @@ -1248,11 +1246,14 @@ pub mod pallet { Ok(()) } - /// Create a new bucket with storage requirements and auto-match to a provider. + /// Create a new bucket with storage and open an agreement with an + /// explicitly chosen provider in one atomic operation. /// - /// This is the preferred way to create a bucket with storage. The system - /// automatically finds a matching provider based on your requirements and - /// creates both the bucket and agreement in one atomic operation. + /// Callers discover providers off-chain via the `find_matching_providers` + /// runtime API, select one, and pass its account here. The pallet performs + /// an O(1) lookup of that single provider and re-validates every constraint + /// (active, accepting primary, duration, price, capacity, stake) before + /// opening the agreement — discovery is advisory, this is the source of truth. /// /// Providers who set `accepting_primary: true` have pre-consented to accepting /// agreements within their stated parameters (capacity, price, duration). @@ -1260,15 +1261,59 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::create_bucket_with_storage())] pub fn create_bucket_with_storage( origin: OriginFor, + provider: T::AccountId, max_bytes: u64, duration: BlockNumberFor, max_price_per_byte: BalanceOf, ) -> DispatchResult { let who = ensure_signed(origin)?; - // Find a matching provider - let (provider, provider_info) = - Self::find_matching_provider(max_bytes, duration, max_price_per_byte)?; + // O(1) lookup of the caller-selected provider, then re-validate every + // constraint against current chain state. + let provider_info = + Providers::::get(&provider).ok_or(Error::::ProviderNotFound)?; + + // Must not be mid-deregistration. + Self::ensure_provider_active(&provider_info)?; + + // Must be accepting primary agreements. + ensure!( + provider_info.settings.accepting_primary, + Error::::ProviderNotAcceptingPrimary + ); + + // Requested duration must be within the provider's accepted range. + Self::validate_duration(&provider_info.settings, duration)?; + + // Provider's price must not exceed the caller's ceiling. + ensure!( + provider_info.settings.price_per_byte <= max_price_per_byte, + Error::::PriceExceedsMax + ); + + // Provider must have enough free capacity for the requested bytes. + let new_committed = provider_info + .committed_bytes + .checked_add(max_bytes) + .ok_or(Error::::ArithmeticOverflow)?; + if provider_info.settings.max_capacity > 0 { + ensure!( + new_committed <= provider_info.settings.max_capacity, + Error::::CapacityExceeded + ); + } + + // Provider must hold enough stake to back the new total commitment. + { + let bytes_as_balance: BalanceOf = new_committed.saturated_into(); + let required_stake = T::MinStakePerByte::get() + .checked_mul(&bytes_as_balance) + .ok_or(Error::::ArithmeticOverflow)?; + ensure!( + provider_info.stake >= required_stake, + Error::::InsufficientStakeForBytes + ); + } // Calculate payment using provider's actual price let payment = Self::calculate_payment( @@ -1807,7 +1852,6 @@ pub mod pallet { // Required stake = committed_bytes * min_stake_per_byte // Using saturated multiplication to avoid overflow - use sp_runtime::traits::SaturatedConversion; let bytes_as_balance: BalanceOf = new_committed_bytes.saturated_into(); let required_stake = T::MinStakePerByte::get() .checked_mul(&bytes_as_balance) @@ -3419,7 +3463,6 @@ pub mod pallet { ) -> Result, DispatchError> { // payment = price_per_byte * max_bytes * duration // Use saturated_from for type conversions - use sp_runtime::traits::SaturatedConversion; let bytes_balance: BalanceOf = max_bytes.saturated_into(); let duration_u128: u128 = duration.saturated_into(); let duration_balance: BalanceOf = duration_u128.saturated_into(); @@ -3430,87 +3473,6 @@ pub mod pallet { .ok_or(Error::::ArithmeticOverflow.into()) } - /// Find a provider matching the storage requirements. - /// - /// Returns the best matching provider that: - /// - Is accepting primary agreements - /// - Has sufficient available capacity - /// - Has price at or below max_price_per_byte - /// - Accepts the requested duration - /// - Has sufficient stake to back the additional bytes - fn find_matching_provider( - bytes_needed: u64, - duration: BlockNumberFor, - max_price_per_byte: BalanceOf, - ) -> Result<(T::AccountId, ProviderInfo), DispatchError> { - use sp_runtime::traits::SaturatedConversion; - - let mut best_match: Option<(T::AccountId, ProviderInfo, BalanceOf)> = None; - - for (account, info) in Providers::::iter() { - // Skip providers in the middle of deregistering. The flag - // check below also catches this (announce forces it false), - // but check explicitly so we don't depend on flag-mutation - // ordering for the security guarantee. - if info.deregister_at.is_some() { - continue; - } - - // Must be accepting primary agreements - if !info.settings.accepting_primary { - continue; - } - - // Check duration constraints - if duration < info.settings.min_duration || duration > info.settings.max_duration { - continue; - } - - // Check price constraint - if info.settings.price_per_byte > max_price_per_byte { - continue; - } - - // Check capacity constraint - let max_capacity = info.settings.max_capacity; - if max_capacity > 0 { - let available = max_capacity.saturating_sub(info.committed_bytes); - if available < bytes_needed { - continue; - } - } - - // Check stake constraint (can they back the additional bytes?) - let new_committed = info.committed_bytes.saturating_add(bytes_needed); - let bytes_as_balance: BalanceOf = new_committed.saturated_into(); - if let Some(required_stake) = - T::MinStakePerByte::get().checked_mul(&bytes_as_balance) - { - if info.stake < required_stake { - continue; - } - } else { - continue; - } - - // This provider matches! Track best by lowest price - let price = info.settings.price_per_byte; - match &best_match { - None => { - best_match = Some((account, info, price)); - } - Some((_, _, best_price)) if price < *best_price => { - best_match = Some((account, info, price)); - } - _ => {} - } - } - - best_match - .map(|(account, info, _)| (account, info)) - .ok_or(Error::::NoMatchingProvider.into()) - } - fn finalize_agreement( bucket_id: BucketId, provider: &T::AccountId, @@ -3633,7 +3595,6 @@ pub mod pallet { let refund_to_owner = if remaining_blocks > Zero::zero() { let total_duration = agreement.expires_at.saturating_sub(agreement.started_at); if total_duration > Zero::zero() { - use sp_runtime::traits::SaturatedConversion; let remaining_u128: u128 = remaining_blocks.saturated_into(); let total_u128: u128 = total_duration.saturated_into(); let payment_u128: u128 = agreement.payment_locked.saturated_into(); @@ -3885,7 +3846,6 @@ pub mod pallet { /// /// Window 0 starts at block 0, window 1 at block `interval`, etc. fn calculate_window(block: BlockNumberFor, interval: BlockNumberFor) -> u64 { - use sp_runtime::traits::SaturatedConversion; if interval.is_zero() { return 0; } @@ -3896,7 +3856,6 @@ pub mod pallet { /// Calculate the start block for a given checkpoint window. fn window_start_block(window: u64, interval: BlockNumberFor) -> BlockNumberFor { - use sp_runtime::traits::SaturatedConversion; let interval_num: u64 = interval.saturated_into(); let start: u64 = window.saturating_mul(interval_num); start.saturated_into() @@ -3952,8 +3911,6 @@ pub mod pallet { pub fn query_provider_info( provider: &T::AccountId, ) -> Option { - use sp_runtime::traits::SaturatedConversion; - Providers::::get(provider).map(|info| { let max_capacity = info.settings.max_capacity; let available_capacity = if max_capacity > 0 { @@ -3994,8 +3951,6 @@ pub mod pallet { offset: u32, limit: u32, ) -> Vec<(T::AccountId, crate::runtime_api::ProviderInfoResponse)> { - use sp_runtime::traits::SaturatedConversion; - Providers::::iter() .skip(offset as usize) .take(limit as usize) @@ -4042,8 +3997,6 @@ pub mod pallet { pub fn query_bucket_info( bucket_id: BucketId, ) -> Option { - use sp_runtime::traits::SaturatedConversion; - Buckets::::get(bucket_id).map(|bucket| crate::runtime_api::BucketResponse { bucket_id, members: bucket @@ -4084,8 +4037,6 @@ pub mod pallet { bucket_id: BucketId, provider: &T::AccountId, ) -> Option { - use sp_runtime::traits::SaturatedConversion; - StorageAgreements::::get(bucket_id, provider).map(|agreement| { crate::runtime_api::AgreementResponse { owner: agreement.owner.encode(), @@ -4119,8 +4070,6 @@ pub mod pallet { pub fn query_bucket_agreements( bucket_id: BucketId, ) -> Vec { - use sp_runtime::traits::SaturatedConversion; - StorageAgreements::::iter_prefix(bucket_id) .map( |(provider, agreement)| crate::runtime_api::AgreementResponse { @@ -4164,8 +4113,6 @@ pub mod pallet { pub fn query_provider_agreements( provider: &T::AccountId, ) -> Vec { - use sp_runtime::traits::SaturatedConversion; - StorageAgreements::::iter() .filter(|(_, p, _)| p == provider) .map( @@ -4202,8 +4149,6 @@ pub mod pallet { pub fn query_challenges_at( block: BlockNumberFor, ) -> Vec { - use sp_runtime::traits::SaturatedConversion; - Challenges::::get(block) .unwrap_or_default() .iter() @@ -4223,8 +4168,6 @@ pub mod pallet { /// Check if provider can accept additional bytes. pub fn query_can_accept_bytes(provider: &T::AccountId, additional_bytes: u64) -> bool { - use sp_runtime::traits::SaturatedConversion; - if let Some(provider_info) = Providers::::get(provider) { let new_committed_bytes = provider_info .committed_bytes @@ -4519,7 +4462,6 @@ pub mod pallet { limit: u32, ) -> Vec { use crate::runtime_api::{MatchedProvider, PartialMatchReason}; - use sp_runtime::traits::SaturatedConversion; let mut results: Vec = Vec::new(); @@ -4636,8 +4578,6 @@ pub mod pallet { offset: u32, limit: u32, ) -> Vec<(T::AccountId, crate::runtime_api::ProviderInfoResponse)> { - use sp_runtime::traits::SaturatedConversion; - Providers::::iter() .filter(|(_, info)| { // Check accepting status diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index 76a1a49d..f713a545 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -402,12 +402,6 @@ mod provider_tests { StorageProvider::accept_agreement(RuntimeOrigin::signed(2), 0), Error::::DeregisterAnnounced ); - - // Auto-match (find_matching_provider) skips deregistering - // providers — request_storage finds no candidate. - // (We don't have a public entry point that calls find_matching_provider - // directly without other setup; the unit-level guarantee is the - // skip branch we added at top of the loop.) }); } @@ -644,8 +638,8 @@ mod provider_tests { 200 )); - // min_duration > max_duration would silently brick the provider - // out of `find_matching_provider`; reject it at the entry point. + // min_duration > max_duration would make the provider impossible to + // match against any duration; reject it at the entry point. let bad_settings = ProviderSettings { min_duration: 1000u64, max_duration: 10u64, @@ -1609,9 +1603,10 @@ mod auto_matching_tests { settings )); - // Create bucket with storage requirements + // Create bucket with storage requirements against the chosen provider assert_ok!(StorageProvider::create_bucket_with_storage( RuntimeOrigin::signed(1), + 2, // explicitly selected provider 100, // max_bytes 100, // duration 10 // max_price_per_byte (higher than provider's price of 0) @@ -1635,12 +1630,18 @@ mod auto_matching_tests { } #[test] - fn create_bucket_with_storage_fails_no_matching_provider() { + fn create_bucket_with_storage_fails_provider_not_found() { new_test_ext().execute_with(|| { - // No providers registered + // Provider 2 is not registered assert_noop!( - StorageProvider::create_bucket_with_storage(RuntimeOrigin::signed(1), 100, 100, 10), - Error::::NoMatchingProvider + StorageProvider::create_bucket_with_storage( + RuntimeOrigin::signed(1), + 2, + 100, + 100, + 10 + ), + Error::::ProviderNotFound ); }); } @@ -1674,8 +1675,14 @@ mod auto_matching_tests { )); assert_noop!( - StorageProvider::create_bucket_with_storage(RuntimeOrigin::signed(1), 100, 100, 10), - Error::::NoMatchingProvider + StorageProvider::create_bucket_with_storage( + RuntimeOrigin::signed(1), + 2, + 100, + 100, + 10 + ), + Error::::ProviderNotAcceptingPrimary ); }); } @@ -1710,11 +1717,12 @@ mod auto_matching_tests { assert_noop!( StorageProvider::create_bucket_with_storage( RuntimeOrigin::signed(1), + 2, 100, 100, 10 // max_price_per_byte is 10, but provider charges 100 ), - Error::::NoMatchingProvider + Error::::PriceExceedsMax ); }); } @@ -1748,11 +1756,12 @@ mod auto_matching_tests { assert_noop!( StorageProvider::create_bucket_with_storage( RuntimeOrigin::signed(1), + 2, 100, // Needs 100 bytes 100, 10 ), - Error::::NoMatchingProvider + Error::::CapacityExceeded ); }); } @@ -1786,22 +1795,23 @@ mod auto_matching_tests { assert_noop!( StorageProvider::create_bucket_with_storage( RuntimeOrigin::signed(1), + 2, 100, 100, // Duration of 100, below provider's min of 500 10 ), - Error::::NoMatchingProvider + Error::::DurationTooShort ); }); } #[test] - fn create_bucket_with_storage_selects_cheapest_provider() { + fn create_bucket_with_storage_honors_explicit_provider() { new_test_ext().execute_with(|| { - // Register two providers with different prices + // Register two eligible providers; the caller picks which one to use. let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - // Provider 2: expensive (price = 5) - but still affordable + // Provider 2: price = 5 assert_ok!(StorageProvider::register_provider( RuntimeOrigin::signed(2), multiaddr.clone().try_into().unwrap(), @@ -1822,7 +1832,7 @@ mod auto_matching_tests { settings_expensive )); - // Provider 3: cheap (price = 0) + // Provider 3: price = 0 assert_ok!(StorageProvider::register_provider( RuntimeOrigin::signed(3), multiaddr.try_into().unwrap(), @@ -1843,18 +1853,20 @@ mod auto_matching_tests { settings_cheap )); - // Create bucket - should match with cheaper provider (3) + // Caller explicitly selects provider 2, even though 3 is cheaper. // Use small values to keep payment low: 10 * 10 * 5 = 500 max assert_ok!(StorageProvider::create_bucket_with_storage( RuntimeOrigin::signed(1), + 2, // explicitly selected provider 10, // max_bytes 10, // duration 10 // max_price_per_byte )); - // Verify matched with provider 3 (the cheaper one) + // Verify the bucket opened the agreement with the chosen provider (2). let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.primary_providers[0], 3); + assert_eq!(bucket.primary_providers[0], 2); + assert!(StorageAgreements::::contains_key(0, 2)); }); } } From 8b2b806bf5154be91edd3ca1fb39ebd49e8a8369 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 11:56:54 +0700 Subject: [PATCH 02/44] feat: update pallet-storage-provider Adding new AgreementTerms type for data signing Adding ReplayWindow and ProviderReplayState to prevent signature replay --- pallet/src/lib.rs | 18 ++++++++++- primitives/src/agreement_term.rs | 37 ++++++++++++++++++++++ primitives/src/lib.rs | 6 ++++ primitives/src/provider_replay_state.rs | 42 +++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 primitives/src/agreement_term.rs create mode 100644 primitives/src/provider_replay_state.rs diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index ec07032d..92d60e1d 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -47,12 +47,21 @@ pub mod pallet { use sp_runtime::traits::{Bounded, CheckedAdd, SaturatedConversion, Saturating, Zero}; use storage_primitives::{ BucketId, BucketSnapshot, ChallengeId, CommitmentPayload, EndAction, MerkleProof, MmrProof, - ProviderRole, RemovalReason, ReplicaRequestParams, Role, HISTORICAL_ROOT_PRIMES, + ProviderRole, RemovalReason, ReplayWindow, ReplicaRequestParams, Role, + HISTORICAL_ROOT_PRIMES, }; pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; + /// Provider-signed agreement quote bound to this pallet's account, balance, + /// and block-number types. + pub type AgreementTermsOf = storage_primitives::AgreementTerms< + ::AccountId, + BalanceOf, + BlockNumberFor, + >; + #[pallet::pallet] pub struct Pallet(_); @@ -159,6 +168,13 @@ pub mod pallet { #[pallet::getter(fn providers)] pub type Providers = StorageMap<_, Blake2_128Concat, T::AccountId, ProviderInfo>; + /// Per-provider sliding replay window over signed agreement-term nonces. + /// See [`storage_primitives::ReplayWindow`] for the bit layout and + /// `establish_agreement` for how the window is enforced. + #[pallet::storage] + pub type ProviderReplayState = + StorageMap<_, Blake2_128Concat, T::AccountId, ReplayWindow, ValueQuery>; + /// Monotonically increasing bucket ID counter. #[pallet::storage] #[pallet::getter(fn next_bucket_id)] diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs new file mode 100644 index 00000000..f63d4879 --- /dev/null +++ b/primitives/src/agreement_term.rs @@ -0,0 +1,37 @@ +//! Provider-signed terms of a storage agreement. +//! +//! A provider quotes terms off-chain (e.g. over HTTP) and signs the SCALE +//! encoding of an `AgreementTerms` value. The owner then submits the signed +//! terms on-chain via `establish_agreement`, which verifies the signature, +//! checks the replay window (see [`crate::provider_replay_state`]), and +//! creates the bucket + agreement atomically. + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use core::fmt::Debug; +use scale_info::TypeInfo; + +/// Off-chain quote signed by the provider and redeemed on-chain by the owner. +/// +/// Generic over the account/balance/block-number types so the same shape can +/// be reused by the pallet (with `BalanceOf`/`BlockNumberFor`), the +/// client SDK, and external tooling. +#[derive( + Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct AgreementTerms { + /// Owner that will be bound by these terms (must match the extrinsic + /// origin at redemption). + pub owner: AccountId, + /// Storage quota committed by the provider, in bytes. + pub max_bytes: u64, + /// Agreement duration in blocks from activation. + pub duration: BlockNumber, + /// Price per byte per block locked at quote time. + pub price_per_byte: Balance, + /// Block number after which the quote is no longer redeemable. + pub valid_until: BlockNumber, + /// Provider-chosen replay-protection nonce; uniqueness is enforced + /// through the provider's sliding replay window. + pub nonce: u64, +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index c521800c..bfb45e2b 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -13,6 +13,12 @@ use core::fmt::Debug; use scale_info::TypeInfo; use sp_core::H256; +pub mod agreement_term; +pub mod provider_replay_state; + +pub use agreement_term::AgreementTerms; +pub use provider_replay_state::{ReplayWindow, REPLAY_WINDOW_BITS}; + /// Bucket ID is a stable, unique identifier (not an index into a collection). /// Using u64 ensures IDs never get reused even if buckets are deleted. pub type BucketId = u64; diff --git a/primitives/src/provider_replay_state.rs b/primitives/src/provider_replay_state.rs new file mode 100644 index 00000000..0c0e9429 --- /dev/null +++ b/primitives/src/provider_replay_state.rs @@ -0,0 +1,42 @@ +//! Per-provider replay protection for signed agreement terms. +//! +//! Each provider maintains a sliding window over the last +//! [`REPLAY_WINDOW_BITS`] nonces it has seen. The window is anchored at the +//! highest-seen nonce (`hwm`) and tracks acceptance state for the inclusive +//! range `hwm - (REPLAY_WINDOW_BITS - 1) ..= hwm`. Nonces older than the +//! window are rejected outright; nonces inside the window are accepted at +//! most once. +//! +//! Bit layout: the LSB of `bitmap[0]` represents `hwm`, the next bit +//! represents `hwm - 1`, and so on. Advancing the window by `d` slots shifts +//! the bitmap left by `d` bits, dropping the oldest entries. + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use core::fmt::Debug; +use scale_info::TypeInfo; + +/// Width of the sliding replay window, in bits / nonce slots. +pub const REPLAY_WINDOW_BITS: u32 = 256; + +/// Sliding replay window tracking the most recent [`REPLAY_WINDOW_BITS`] +/// nonces a provider has signed. +#[derive( + Clone, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + TypeInfo, + MaxEncodedLen, + Debug, + Default, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ReplayWindow { + /// Highest nonce ever accepted for this provider (window anchor). + pub hwm: u64, + /// 256-bit acceptance bitmap; bit `i` (counting from the LSB of + /// `bitmap[0]`) is set iff nonce `hwm - i` has been accepted. + pub bitmap: [u8; 32], +} From c9d0edd3b99103768243355e4e84d843447d75b2 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 14:40:42 +0700 Subject: [PATCH 03/44] feat: add establish_storage_agreement with provider-signed terms Introduce a single-call flow where a provider signs storage terms off-chain and the owner redeems them on-chain, replacing the request/accept dance for primary agreements. Bucket creation and agreement opening are folded into one atomic extrinsic. Primitives (`storage-primitives`): - `AgreementTerms`: provider-signed quote carrying owner, max_bytes, duration, price_per_byte, valid_until, nonce. - `ReplayWindow`: per-provider 256-slot sliding window over signed nonces (`hwm` + 32-byte bitmap, LSB = hwm). `try_accept(nonce)` shifts the bitmap on forward jumps and rejects duplicates (`AlreadyUsed`) or out-of-window pasts (`TooOld`). Covered by 7 unit tests including out-of-order, edge, and large-jump cases. Pallet (`pallet-storage-provider`): - `ProviderReplayState` storage map (`AccountId -> ReplayWindow`). - `establish_storage_agreement` extrinsic + pub `establish_storage_agreement_internal(owner, provider, terms, sig)` helper for Layer 1 reuse. Verifies `MultiSignature` over `blake2_256(SCALE(terms))`, checks `valid_until`, advances the replay window, then runs the existing provider/capacity/stake/duration/price validation before creating the bucket + primary agreement. - New errors: `InvalidProviderSignature`, `TermsExpired`, `NonceAlreadyUsed`, `NonceTooOld`, `TermsOwnerMismatch`. - New event: `StorageAgreementEstablished { bucket_id, provider, owner, terms, expires_at }`. Named `Storage*` so a future `establish_replica_sync_agreement` flow can sit alongside. The legacy `create_bucket_with_storage` extrinsic is left in place for now; it will be removed in a follow-up once callers migrate. --- pallet/src/lib.rs | 234 +++++++++++++++++++++++- primitives/src/agreement_term.rs | 2 +- primitives/src/lib.rs | 2 +- primitives/src/provider_replay_state.rs | 182 ++++++++++++++++-- 4 files changed, 405 insertions(+), 15 deletions(-) diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 92d60e1d..6c3c3634 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -47,7 +47,7 @@ pub mod pallet { use sp_runtime::traits::{Bounded, CheckedAdd, SaturatedConversion, Saturating, Zero}; use storage_primitives::{ BucketId, BucketSnapshot, ChallengeId, CommitmentPayload, EndAction, MerkleProof, MmrProof, - ProviderRole, RemovalReason, ReplayWindow, ReplicaRequestParams, Role, + ProviderRole, RemovalReason, ReplayError, ReplayWindow, ReplicaRequestParams, Role, HISTORICAL_ROOT_PRIMES, }; @@ -170,7 +170,7 @@ pub mod pallet { /// Per-provider sliding replay window over signed agreement-term nonces. /// See [`storage_primitives::ReplayWindow`] for the bit layout and - /// `establish_agreement` for how the window is enforced. + /// `establish_storage_agreement` for how the window is enforced. #[pallet::storage] pub type ProviderReplayState = StorageMap<_, Blake2_128Concat, T::AccountId, ReplayWindow, ValueQuery>; @@ -679,6 +679,15 @@ pub mod pallet { provider: T::AccountId, payment_to_provider: BalanceOf, }, + /// Owner redeemed provider-signed terms; bucket created and agreement + /// opened atomically. + StorageAgreementEstablished { + bucket_id: BucketId, + provider: T::AccountId, + owner: T::AccountId, + terms: AgreementTermsOf, + expires_at: BlockNumberFor, + }, // Challenge events ChallengeCreated { @@ -853,6 +862,20 @@ pub mod pallet { /// The selected provider's price per byte exceeds the caller's /// `max_price_per_byte`. PriceExceedsMax, + + // establish_storage_agreement errors + /// Provider signature over the SCALE-encoded terms is invalid. + InvalidProviderSignature, + /// Signed terms have passed their `valid_until` block. + TermsExpired, + /// The terms' nonce has already been consumed inside the provider's + /// replay window. + NonceAlreadyUsed, + /// The terms' nonce is older than the provider's replay window + /// (distance from `hwm` ≥ [`storage_primitives::REPLAY_WINDOW_BITS`]). + NonceTooOld, + /// The terms' declared owner does not match the extrinsic origin. + TermsOwnerMismatch, } // ───────────────────────────────────────────────────────────────────────── @@ -1426,6 +1449,27 @@ pub mod pallet { Ok(()) } + /// Redeem provider-signed terms: create a bucket + primary agreement + /// in a single call. + /// + /// The provider signs a SCALE-encoded [`AgreementTermsOf`] off-chain; + /// the owner submits it here. The pallet verifies the signature, + /// rejects replays via the provider's sliding nonce window, then runs + /// the standard provider/capacity/stake checks and opens the + /// agreement. + #[pallet::call_index(17)] + #[pallet::weight(10_000)] + pub fn establish_storage_agreement( + origin: OriginFor, + provider: T::AccountId, + terms: AgreementTermsOf, + sig: sp_runtime::MultiSignature, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::establish_storage_agreement_internal(&who, &provider, terms, &sig)?; + Ok(()) + } + /// Set minimum providers required for checkpoint. #[pallet::call_index(11)] #[pallet::weight(T::WeightInfo::set_bucket_min_providers())] @@ -3315,6 +3359,45 @@ pub mod pallet { Ok(()) } + /// Verify a provider signature over a SCALE-encoded + /// [`AgreementTermsOf`]. The signed payload is + /// `blake2_256(terms.encode())`. + fn verify_terms_signature( + provider_info: &ProviderInfo, + terms: &AgreementTermsOf, + sig: &sp_runtime::MultiSignature, + ) -> DispatchResult { + use sp_runtime::traits::Verify; + + let public_key_bytes = provider_info.public_key.as_slice(); + let account_id = match sig { + sp_runtime::MultiSignature::Sr25519(_) | sp_runtime::MultiSignature::Ed25519(_) => { + ensure!( + public_key_bytes.len() == 32, + Error::::InvalidPublicKey + ); + let mut key_bytes = [0u8; 32]; + key_bytes.copy_from_slice(public_key_bytes); + sp_runtime::AccountId32::new(key_bytes) + } + sp_runtime::MultiSignature::Ecdsa(_) | sp_runtime::MultiSignature::Eth(_) => { + ensure!( + public_key_bytes.len() == 33, + Error::::InvalidPublicKey + ); + let hash = sp_io::hashing::blake2_256(public_key_bytes); + sp_runtime::AccountId32::new(hash) + } + }; + + let hash = sp_io::hashing::blake2_256(&terms.encode()); + ensure!( + sig.verify(&hash[..], &account_id), + Error::::InvalidProviderSignature + ); + Ok(()) + } + fn ensure_admin(who: &T::AccountId, bucket: &Bucket) -> DispatchResult { ensure!( bucket @@ -4265,6 +4348,153 @@ pub mod pallet { Ok(bucket_id) } + /// Redeem provider-signed terms (used directly by the + /// `establish_storage_agreement` extrinsic and by higher-layer pallets that + /// fold bucket creation into their own flows). + /// + /// Verifies the signature, advances the provider's replay window, + /// then runs the same provider/capacity/stake checks as + /// `create_bucket_with_storage` before creating the bucket + primary + /// agreement. + pub fn establish_storage_agreement_internal( + owner: &T::AccountId, + provider: &T::AccountId, + terms: AgreementTermsOf, + sig: &sp_runtime::MultiSignature, + ) -> Result { + // Origin must match the owner the provider signed for. + ensure!(&terms.owner == owner, Error::::TermsOwnerMismatch); + + // Quote must not be stale. + let current_block = frame_system::Pallet::::block_number(); + ensure!( + terms.valid_until >= current_block, + Error::::TermsExpired + ); + + // Provider lookup + signature check over blake2_256(SCALE(terms)). + let provider_info = + Providers::::get(provider).ok_or(Error::::ProviderNotFound)?; + Self::verify_terms_signature(&provider_info, &terms, sig)?; + + // Replay window: at most once per nonce, within the trailing 256 slots. + ProviderReplayState::::try_mutate(provider, |window| -> DispatchResult { + window.try_accept(terms.nonce).map_err(|e| match e { + ReplayError::AlreadyUsed => Error::::NonceAlreadyUsed, + ReplayError::TooOld => Error::::NonceTooOld, + })?; + Ok(()) + })?; + + // Validate on-chain provider's state then create bucket + Self::ensure_provider_active(&provider_info)?; + ensure!( + provider_info.settings.accepting_primary, + Error::::ProviderNotAcceptingPrimary + ); + Self::validate_duration(&provider_info.settings, terms.duration)?; + ensure!( + provider_info.settings.price_per_byte <= terms.price_per_byte, + Error::::PriceExceedsMax + ); + + let new_committed = provider_info + .committed_bytes + .checked_add(terms.max_bytes) + .ok_or(Error::::ArithmeticOverflow)?; + if provider_info.settings.max_capacity > 0 { + ensure!( + new_committed <= provider_info.settings.max_capacity, + Error::::CapacityExceeded + ); + } + + { + let bytes_as_balance: BalanceOf = new_committed.saturated_into(); + let required_stake = T::MinStakePerByte::get() + .checked_mul(&bytes_as_balance) + .ok_or(Error::::ArithmeticOverflow)?; + ensure!( + provider_info.stake >= required_stake, + Error::::InsufficientStakeForBytes + ); + } + + // Pay at the price the provider signed for. + let payment = + Self::calculate_payment(terms.price_per_byte, terms.max_bytes, terms.duration)?; + T::Currency::reserve(owner, payment)?; + + // Bucket creation folded in: owner is sole admin, provider is the + // bucket's single primary. + let bucket_id = NextBucketId::::get(); + NextBucketId::::put(bucket_id.saturating_add(1)); + + let admin_member = Member { + account: owner.clone(), + role: Role::Admin, + }; + let mut members = BoundedVec::new(); + members + .try_push(admin_member) + .map_err(|_| Error::::MaxMembersReached)?; + let mut primary_providers = BoundedVec::new(); + primary_providers + .try_push(provider.clone()) + .map_err(|_| Error::::MaxPrimaryProvidersReached)?; + + let bucket = Bucket { + members, + frozen_start_seq: None, + min_providers: 1, + primary_providers, + snapshot: None, + historical_roots: [(0, H256::zero()); 6], + total_snapshots: 0, + }; + Buckets::::insert(bucket_id, bucket); + + MemberBuckets::::try_mutate(owner, |buckets| { + buckets + .try_push(bucket_id) + .map_err(|_| Error::::TooManyBucketsForMember) + })?; + + let expires_at = current_block.saturating_add(terms.duration); + let agreement = StorageAgreement { + owner: owner.clone(), + max_bytes: terms.max_bytes, + payment_locked: payment, + price_per_byte: terms.price_per_byte, + expires_at, + extensions_blocked: false, + role: ProviderRole::Primary, + started_at: current_block, + }; + + Providers::::mutate(provider, |maybe_provider| { + if let Some(p) = maybe_provider { + p.committed_bytes = p.committed_bytes.saturating_add(terms.max_bytes); + p.stats.agreements_total = p.stats.agreements_total.saturating_add(1); + } + }); + StorageAgreements::::insert(bucket_id, provider, agreement); + + Self::deposit_event(Event::BucketCreated { + bucket_id, + admin: owner.clone(), + }); + Self::deposit_event(Event::StorageAgreementEstablished { + bucket_id, + provider: provider.clone(), + owner: owner.clone(), + terms, + expires_at, + }); + + Ok(bucket_id) + } + /// Request a primary storage agreement internally (for use by other pallets). /// /// This creates a primary storage agreement without requiring admin origin check. diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs index f63d4879..b1bf3cdd 100644 --- a/primitives/src/agreement_term.rs +++ b/primitives/src/agreement_term.rs @@ -2,7 +2,7 @@ //! //! A provider quotes terms off-chain (e.g. over HTTP) and signs the SCALE //! encoding of an `AgreementTerms` value. The owner then submits the signed -//! terms on-chain via `establish_agreement`, which verifies the signature, +//! terms on-chain via `establish_storage_agreement`, which verifies the signature, //! checks the replay window (see [`crate::provider_replay_state`]), and //! creates the bucket + agreement atomically. diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index bfb45e2b..62d0452b 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -17,7 +17,7 @@ pub mod agreement_term; pub mod provider_replay_state; pub use agreement_term::AgreementTerms; -pub use provider_replay_state::{ReplayWindow, REPLAY_WINDOW_BITS}; +pub use provider_replay_state::{ReplayError, ReplayWindow, REPLAY_WINDOW_BITS}; /// Bucket ID is a stable, unique identifier (not an index into a collection). /// Using u64 ensures IDs never get reused even if buckets are deleted. diff --git a/primitives/src/provider_replay_state.rs b/primitives/src/provider_replay_state.rs index 0c0e9429..06c7e801 100644 --- a/primitives/src/provider_replay_state.rs +++ b/primitives/src/provider_replay_state.rs @@ -1,15 +1,17 @@ //! Per-provider replay protection for signed agreement terms. //! -//! Each provider maintains a sliding window over the last -//! [`REPLAY_WINDOW_BITS`] nonces it has seen. The window is anchored at the -//! highest-seen nonce (`hwm`) and tracks acceptance state for the inclusive -//! range `hwm - (REPLAY_WINDOW_BITS - 1) ..= hwm`. Nonces older than the -//! window are rejected outright; nonces inside the window are accepted at -//! most once. +//! Each provider maintains a sliding window over the most recent +//! [`REPLAY_WINDOW_BITS`] nonces it has issued. The window is anchored at the +//! highest nonce ever accepted (`hwm`) and tracks the inclusive range +//! `hwm - (REPLAY_WINDOW_BITS - 1) ..= hwm`. Nonces older than the window +//! are rejected outright; nonces inside the window are accepted at most +//! once. //! -//! Bit layout: the LSB of `bitmap[0]` represents `hwm`, the next bit -//! represents `hwm - 1`, and so on. Advancing the window by `d` slots shifts -//! the bitmap left by `d` bits, dropping the oldest entries. +//! # Bit layout +//! +//! The LSB of `bitmap[0]` represents `hwm`, the next bit represents +//! `hwm - 1`, and so on. Advancing the window by `d` slots shifts the +//! bitmap left by `d` bits, dropping the oldest entries. use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::fmt::Debug; @@ -18,8 +20,8 @@ use scale_info::TypeInfo; /// Width of the sliding replay window, in bits / nonce slots. pub const REPLAY_WINDOW_BITS: u32 = 256; -/// Sliding replay window tracking the most recent [`REPLAY_WINDOW_BITS`] -/// nonces a provider has signed. +/// Sliding replay window over the most recent [`REPLAY_WINDOW_BITS`] nonces +/// accepted from a provider. #[derive( Clone, PartialEq, @@ -40,3 +42,161 @@ pub struct ReplayWindow { /// `bitmap[0]`) is set iff nonce `hwm - i` has been accepted. pub bitmap: [u8; 32], } + +/// Reasons [`ReplayWindow::try_accept`] can reject a nonce. +#[derive(Debug, PartialEq, Eq)] +pub enum ReplayError { + /// The nonce is inside the window and has already been marked as seen. + AlreadyUsed, + /// The nonce is more than [`REPLAY_WINDOW_BITS`] slots behind `hwm`. + TooOld, +} + +impl ReplayWindow { + /// Records `nonce` as seen, advancing the window if necessary. + /// + /// When `nonce > hwm`, the bitmap is shifted left by `nonce - hwm` and + /// `hwm` is updated; the new high-water mark is then marked at bit 0. + /// When `nonce <= hwm`, the bit at distance `hwm - nonce` is set. + /// + /// # Errors + /// + /// * [`ReplayError::AlreadyUsed`] if the nonce is inside the window + /// and its bit is already set. + /// * [`ReplayError::TooOld`] if the nonce is more than + /// [`REPLAY_WINDOW_BITS`] slots behind `hwm`. + pub fn try_accept(&mut self, nonce: u64) -> Result<(), ReplayError> { + if nonce > self.hwm { + let shift = nonce - self.hwm; + shift_left_le(&mut self.bitmap, shift); + self.hwm = nonce; + self.bitmap[0] |= 1; + return Ok(()); + } + + let distance = self.hwm - nonce; + if distance >= REPLAY_WINDOW_BITS as u64 { + return Err(ReplayError::TooOld); + } + let byte = (distance / 8) as usize; + let mask = 1u8 << (distance % 8); + if self.bitmap[byte] & mask != 0 { + return Err(ReplayError::AlreadyUsed); + } + self.bitmap[byte] |= mask; + Ok(()) + } +} + +/// Shifts a 256-bit little-endian bitmap left by `shift` bits. +/// +/// Bit `i` (counting from the LSB of `bytes[0]`) moves to position +/// `i + shift`; positions `< shift` are cleared. Shifts of +/// [`REPLAY_WINDOW_BITS`] or more clear the entire bitmap. +fn shift_left_le(bytes: &mut [u8; 32], shift: u64) { + if shift == 0 { + return; + } + if shift >= REPLAY_WINDOW_BITS as u64 { + *bytes = [0u8; 32]; + return; + } + let byte_shift = (shift / 8) as usize; + let bit_shift = (shift % 8) as u32; + + let mut out = [0u8; 32]; + if bit_shift == 0 { + for i in byte_shift..32 { + out[i] = bytes[i - byte_shift]; + } + } else { + for i in byte_shift..32 { + let src = i - byte_shift; + let lo = bytes[src] << bit_shift; + let hi = if src > 0 { + bytes[src - 1] >> (8 - bit_shift) + } else { + 0 + }; + out[i] = lo | hi; + } + } + *bytes = out; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn first_nonce_sets_hwm() { + let mut w = ReplayWindow::default(); + assert!(w.try_accept(5).is_ok()); + assert_eq!(w.hwm, 5); + assert_eq!(w.bitmap[0] & 1, 1); + } + + #[test] + fn duplicate_nonce_rejected() { + let mut w = ReplayWindow::default(); + w.try_accept(10).unwrap(); + assert_eq!(w.try_accept(10), Err(ReplayError::AlreadyUsed)); + } + + #[test] + fn out_of_order_nonces_accepted() { + let mut w = ReplayWindow::default(); + for n in [3u64, 7, 1, 10, 2] { + assert!(w.try_accept(n).is_ok(), "nonce {n} should be accepted"); + } + assert_eq!(w.hwm, 10); + for n in [3u64, 7, 1, 10, 2] { + assert_eq!(w.try_accept(n), Err(ReplayError::AlreadyUsed)); + } + } + + #[test] + fn nonce_at_window_edge_accepted() { + let mut w = ReplayWindow::default(); + w.try_accept(300).unwrap(); + // Distance 255 is still inside the window. + assert!(w.try_accept(300 - 255).is_ok()); + } + + #[test] + fn nonce_past_window_edge_rejected() { + let mut w = ReplayWindow::default(); + w.try_accept(300).unwrap(); + // Distance 256 is just past the window. + assert_eq!(w.try_accept(300 - 256), Err(ReplayError::TooOld)); + // Distance much greater than 256. + assert_eq!(w.try_accept(1), Err(ReplayError::TooOld)); + } + + #[test] + fn forward_jump_beyond_window_clears_bitmap() { + let mut w = ReplayWindow::default(); + w.try_accept(5).unwrap(); + w.try_accept(7).unwrap(); + w.try_accept(1000).unwrap(); + assert_eq!(w.hwm, 1000); + assert_eq!(w.bitmap[0], 1); + for b in &w.bitmap[1..] { + assert_eq!(*b, 0); + } + } + + #[test] + fn bitmap_shifts_track_distances() { + let mut w = ReplayWindow::default(); + w.try_accept(100).unwrap(); + w.try_accept(101).unwrap(); + // 101 is hwm; 100 sits at distance 1, so bits 0 and 1 of byte 0 are set. + assert_eq!(w.bitmap[0] & 0b11, 0b11); + w.try_accept(109).unwrap(); + // After shifting left by 8, the bits previously at positions 0 and 1 + // land in byte 1 at positions 0 and 1; bit 0 of byte 0 marks the new hwm. + assert_eq!(w.bitmap[0] & 1, 1); + assert_eq!(w.bitmap[1] & 0b11, 0b11); + } +} From 43eeb2ed1b7d4ced553ae025fddaa00f351d86ce Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 15:57:59 +0700 Subject: [PATCH 04/44] feat: migrate request_agreement to establish_replica_agreement Refactor replica agreement creation to use the same provider-signed terms flow as establish_storage_agreement, eliminating the pending request/accept stage. - recover `replica_params` in `AgreementTerms` - migrate `request_agreement` extrinsic to `establish_replica_agreement`: redeems provider-signed terms against an existing bucket, verifying signature and replay window before opening the agreement atomically. - migrate `request_replica_agreement_internal` to `establish_replica_agreement_internal`: mirrors `establish_storage_agreement_internal` for higher-layer pallets. - Drop the `AgreementRequest` storage and struct, the cleanup_bucket drain loop, the `AgreementRequested`/`AgreementRejected`/`AgreementRequestWithdrawn` events, and the now-unused request-related errors. - Add `ReplicaAgreementEstablished` event and `MissingReplicaTerms` error. The pallets in storage-interfaces/, benchmarks, tests, and client SDK still reference the old names and need a follow-up pass. --- pallet/src/lib.rs | 752 ++++++------------------------- primitives/src/agreement_term.rs | 30 +- primitives/src/lib.rs | 16 +- 3 files changed, 169 insertions(+), 629 deletions(-) diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 6c3c3634..6f9e4610 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -47,8 +47,7 @@ pub mod pallet { use sp_runtime::traits::{Bounded, CheckedAdd, SaturatedConversion, Saturating, Zero}; use storage_primitives::{ BucketId, BucketSnapshot, ChallengeId, CommitmentPayload, EndAction, MerkleProof, MmrProof, - ProviderRole, RemovalReason, ReplayError, ReplayWindow, ReplicaRequestParams, Role, - HISTORICAL_ROOT_PRIMES, + ProviderRole, RemovalReason, ReplayError, ReplayWindow, Role, HISTORICAL_ROOT_PRIMES, }; pub type BalanceOf = @@ -198,18 +197,6 @@ pub mod pallet { StorageAgreement, >; - /// Pending agreement requests (bucket → provider → request). - #[pallet::storage] - #[pallet::getter(fn agreement_requests)] - pub type AgreementRequests = StorageDoubleMap< - _, - Blake2_128Concat, - BucketId, - Blake2_128Concat, - T::AccountId, - AgreementRequest, - >; - /// Pending challenges indexed by deadline block. #[pallet::storage] #[pallet::unbounded] @@ -269,28 +256,6 @@ pub mod pallet { ValueQuery, >; - // ───────────────────────────────────────────────────────────────────────── - // Genesis Config - // ───────────────────────────────────────────────────────────────────────── - - /// Genesis configuration for the storage provider pallet. - #[pallet::genesis_config] - #[derive(DefaultNoBound)] - pub struct GenesisConfig { - /// Buckets to create at genesis: (admin_account, min_providers). - pub buckets: Vec<(T::AccountId, u32)>, - } - - #[pallet::genesis_build] - impl BuildGenesisConfig for GenesisConfig { - fn build(&self) { - for (admin, min_providers) in &self.buckets { - Pallet::::create_bucket_internal(admin, *min_providers) - .expect("genesis bucket creation should not fail"); - } - } - } - // ───────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────── @@ -447,24 +412,6 @@ pub mod pallet { pub started_at: BlockNumberFor, } - /// Pending agreement request. - #[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen, Debug)] - #[scale_info(skip_type_params(T))] - pub struct AgreementRequest { - /// Who requested the agreement. - pub requester: T::AccountId, - /// Maximum bytes requested. - pub max_bytes: u64, - /// Payment locked by requester. - pub payment_locked: BalanceOf, - /// Requested duration. - pub duration: BlockNumberFor, - /// Block at which request expires. - pub expires_at: BlockNumberFor, - /// Replica-specific parameters, None for primary agreements. - pub replica_params: Option, BlockNumberFor>>, - } - /// Active challenge against a provider. #[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen, Debug)] #[scale_info(skip_type_params(T))] @@ -627,29 +574,11 @@ pub mod pallet { }, // Agreement events - AgreementRequested { - bucket_id: BucketId, - provider: T::AccountId, - requester: T::AccountId, - max_bytes: u64, - payment_locked: BalanceOf, - duration: BlockNumberFor, - }, AgreementAccepted { bucket_id: BucketId, provider: T::AccountId, expires_at: BlockNumberFor, }, - AgreementRejected { - bucket_id: BucketId, - provider: T::AccountId, - payment_returned: BalanceOf, - }, - AgreementRequestWithdrawn { - bucket_id: BucketId, - provider: T::AccountId, - payment_returned: BalanceOf, - }, AgreementToppedUp { bucket_id: BucketId, provider: T::AccountId, @@ -688,6 +617,15 @@ pub mod pallet { terms: AgreementTermsOf, expires_at: BlockNumberFor, }, + /// Owner redeemed provider-signed replica terms; replica agreement + /// opened against an existing bucket. + ReplicaAgreementEstablished { + bucket_id: BucketId, + provider: T::AccountId, + owner: T::AccountId, + terms: AgreementTermsOf, + expires_at: BlockNumberFor, + }, // Challenge events ChallengeCreated { @@ -794,9 +732,7 @@ pub mod pallet { // Agreement errors AgreementNotFound, - AgreementRequestNotFound, AgreementAlreadyExists, - AgreementRequestAlreadyExists, AgreementExpired, AgreementNotExpired, AgreementExtensionsBlocked, @@ -804,7 +740,6 @@ pub mod pallet { DurationTooShort, DurationTooLong, PaymentExceedsMax, - RequestExpired, CannotTerminateReplica, SettlementWindowPassed, @@ -876,6 +811,9 @@ pub mod pallet { NonceTooOld, /// The terms' declared owner does not match the extrinsic origin. TermsOwnerMismatch, + /// Replica terms missing from a signed quote redeemed as a replica + /// agreement. + MissingReplicaTerms, } // ───────────────────────────────────────────────────────────────────────── @@ -1693,342 +1631,25 @@ pub mod pallet { // Storage Agreements // ───────────────────────────────────────────────────────────────────── - /// Request a replica storage agreement. + /// Redeem provider-signed terms for a replica storage agreement. + /// + /// The provider signs a SCALE-encoded [`AgreementTermsOf`] with + /// `replica_params: Some(_)` off-chain; the owner submits it here. + /// The pallet verifies the signature, rejects replays via the + /// provider's sliding nonce window, then runs the standard + /// provider/capacity/stake checks and opens the replica agreement on + /// an existing bucket. #[pallet::call_index(20)] - #[pallet::weight(T::WeightInfo::request_agreement())] - pub fn request_agreement( - origin: OriginFor, - bucket_id: BucketId, - provider: T::AccountId, - max_bytes: u64, - duration: BlockNumberFor, - max_payment: BalanceOf, - replica_params: ReplicaRequestParams, BlockNumberFor>, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - ensure!( - Buckets::::contains_key(bucket_id), - Error::::BucketNotFound - ); - - let provider_info = - Providers::::get(&provider).ok_or(Error::::ProviderNotFound)?; - Self::ensure_provider_active(&provider_info)?; - - ensure!( - provider_info.settings.replica_sync_price.is_some(), - Error::::ProviderNotAcceptingReplicas - ); - - Self::validate_duration(&provider_info.settings, duration)?; - - // Calculate payment - let payment = Self::calculate_payment( - provider_info.settings.price_per_byte, - max_bytes, - duration, - )?; - ensure!(payment <= max_payment, Error::::PaymentExceedsMax); - - // Total to lock = storage payment + sync balance - let total_lock = payment - .checked_add(&replica_params.sync_balance) - .ok_or(Error::::ArithmeticOverflow)?; - - // Reserve funds - T::Currency::reserve(&who, total_lock)?; - - let current_block = frame_system::Pallet::::block_number(); - let expires_at = current_block.saturating_add(T::RequestTimeout::get()); - - let request = AgreementRequest { - requester: who.clone(), - max_bytes, - payment_locked: payment, - duration, - expires_at, - replica_params: Some(replica_params), - }; - - ensure!( - !AgreementRequests::::contains_key(bucket_id, &provider), - Error::::AgreementRequestAlreadyExists - ); - - AgreementRequests::::insert(bucket_id, &provider, request); - - Self::deposit_event(Event::AgreementRequested { - bucket_id, - provider, - requester: who, - max_bytes, - payment_locked: payment, - duration, - }); - - Ok(()) - } - - /// Request a primary storage agreement (admin only). - #[pallet::call_index(21)] - #[pallet::weight(T::WeightInfo::request_primary_agreement())] - pub fn request_primary_agreement( - origin: OriginFor, - bucket_id: BucketId, - provider: T::AccountId, - max_bytes: u64, - duration: BlockNumberFor, - max_payment: BalanceOf, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - let bucket = Buckets::::get(bucket_id).ok_or(Error::::BucketNotFound)?; - - Self::ensure_admin(&who, &bucket)?; - - // Check primary provider limit - ensure!( - bucket.primary_providers.len() < T::MaxPrimaryProviders::get() as usize, - Error::::MaxPrimaryProvidersReached - ); - - let provider_info = - Providers::::get(&provider).ok_or(Error::::ProviderNotFound)?; - Self::ensure_provider_active(&provider_info)?; - - ensure!( - provider_info.settings.accepting_primary, - Error::::ProviderNotAcceptingPrimary - ); - - Self::validate_duration(&provider_info.settings, duration)?; - - let payment = Self::calculate_payment( - provider_info.settings.price_per_byte, - max_bytes, - duration, - )?; - ensure!(payment <= max_payment, Error::::PaymentExceedsMax); - - T::Currency::reserve(&who, payment)?; - - let current_block = frame_system::Pallet::::block_number(); - let expires_at = current_block.saturating_add(T::RequestTimeout::get()); - - let request = AgreementRequest { - requester: who.clone(), - max_bytes, - payment_locked: payment, - duration, - expires_at, - replica_params: None, // Primary agreement - }; - - ensure!( - !AgreementRequests::::contains_key(bucket_id, &provider), - Error::::AgreementRequestAlreadyExists - ); - - AgreementRequests::::insert(bucket_id, &provider, request); - - Self::deposit_event(Event::AgreementRequested { - bucket_id, - provider, - requester: who, - max_bytes, - payment_locked: payment, - duration, - }); - - Ok(()) - } - - /// Accept a pending agreement request. - #[pallet::call_index(22)] - #[pallet::weight(T::WeightInfo::accept_agreement())] - pub fn accept_agreement(origin: OriginFor, bucket_id: BucketId) -> DispatchResult { - let who = ensure_signed(origin)?; - - let provider_info = Providers::::get(&who).ok_or(Error::::ProviderNotFound)?; - Self::ensure_provider_active(&provider_info)?; - - let request = AgreementRequests::::take(bucket_id, &who) - .ok_or(Error::::AgreementRequestNotFound)?; - - let current_block = frame_system::Pallet::::block_number(); - ensure!( - current_block <= request.expires_at, - Error::::RequestExpired - ); - - let expires_at = current_block.saturating_add(request.duration); - - // Create the role based on whether replica params exist - let role = if let Some(replica_params) = request.replica_params { - let provider_info = - Providers::::get(&who).ok_or(Error::::ProviderNotFound)?; - let sync_price = provider_info - .settings - .replica_sync_price - .ok_or(Error::::ProviderNotAcceptingReplicas)?; - - ProviderRole::Replica { - sync_balance: replica_params.sync_balance, - sync_price, - min_sync_interval: replica_params.min_sync_interval, - last_sync: None, - } - } else { - // Primary: add to bucket's primary_providers - Buckets::::try_mutate(bucket_id, |maybe_bucket| -> DispatchResult { - let bucket = maybe_bucket.as_mut().ok_or(Error::::BucketNotFound)?; - bucket - .primary_providers - .try_push(who.clone()) - .map_err(|_| Error::::MaxPrimaryProvidersReached)?; - Ok(()) - })?; - - ProviderRole::Primary - }; - - let provider_info = Providers::::get(&who).ok_or(Error::::ProviderNotFound)?; - - // Enforce stake-to-bytes ratio - // New commitment = existing + requested - let new_committed_bytes = provider_info - .committed_bytes - .checked_add(request.max_bytes) - .ok_or(Error::::ArithmeticOverflow)?; - - // Check capacity constraint (if max_capacity > 0) - if provider_info.settings.max_capacity > 0 { - ensure!( - new_committed_bytes <= provider_info.settings.max_capacity, - Error::::CapacityExceeded - ); - } - - // Required stake = committed_bytes * min_stake_per_byte - // Using saturated multiplication to avoid overflow - let bytes_as_balance: BalanceOf = new_committed_bytes.saturated_into(); - let required_stake = T::MinStakePerByte::get() - .checked_mul(&bytes_as_balance) - .ok_or(Error::::ArithmeticOverflow)?; - - ensure!( - provider_info.stake >= required_stake, - Error::::InsufficientStakeForBytes - ); - - let agreement = StorageAgreement { - owner: request.requester, - max_bytes: request.max_bytes, - payment_locked: request.payment_locked, - price_per_byte: provider_info.settings.price_per_byte, - expires_at, - extensions_blocked: false, - role, - started_at: current_block, - }; - - // Update provider stats - Providers::::mutate(&who, |maybe_provider| { - if let Some(provider) = maybe_provider { - provider.committed_bytes = - provider.committed_bytes.saturating_add(request.max_bytes); - provider.stats.agreements_total = - provider.stats.agreements_total.saturating_add(1); - provider.stats.total_bytes_committed = provider - .stats - .total_bytes_committed - .saturating_add(request.max_bytes); - } - }); - - StorageAgreements::::insert(bucket_id, &who, agreement); - - Self::deposit_event(Event::AgreementAccepted { - bucket_id, - provider: who.clone(), - expires_at, - }); - - Self::deposit_event(Event::ProviderAddedToBucket { - bucket_id, - provider: who, - }); - - Ok(()) - } - - /// Reject a pending agreement request. - #[pallet::call_index(23)] - #[pallet::weight(T::WeightInfo::reject_agreement())] - pub fn reject_agreement(origin: OriginFor, bucket_id: BucketId) -> DispatchResult { - let who = ensure_signed(origin)?; - - let request = AgreementRequests::::take(bucket_id, &who) - .ok_or(Error::::AgreementRequestNotFound)?; - - // Calculate total locked (storage payment + sync balance for replicas) - let total_locked = if let Some(ref replica_params) = request.replica_params { - request - .payment_locked - .checked_add(&replica_params.sync_balance) - .ok_or(Error::::ArithmeticOverflow)? - } else { - request.payment_locked - }; - - // Return funds to requester - T::Currency::unreserve(&request.requester, total_locked); - - Self::deposit_event(Event::AgreementRejected { - bucket_id, - provider: who, - payment_returned: total_locked, - }); - - Ok(()) - } - - /// Withdraw a pending agreement request. - #[pallet::call_index(24)] - #[pallet::weight(T::WeightInfo::withdraw_agreement_request())] - pub fn withdraw_agreement_request( + #[pallet::weight(10_000)] + pub fn establish_replica_agreement( origin: OriginFor, bucket_id: BucketId, provider: T::AccountId, + terms: AgreementTermsOf, + sig: sp_runtime::MultiSignature, ) -> DispatchResult { let who = ensure_signed(origin)?; - - let request = AgreementRequests::::get(bucket_id, &provider) - .ok_or(Error::::AgreementRequestNotFound)?; - - ensure!(request.requester == who, Error::::NotAgreementOwner); - - AgreementRequests::::remove(bucket_id, &provider); - - // Calculate total locked - let total_locked = if let Some(ref replica_params) = request.replica_params { - request - .payment_locked - .checked_add(&replica_params.sync_balance) - .ok_or(Error::::ArithmeticOverflow)? - } else { - request.payment_locked - }; - - T::Currency::unreserve(&who, total_locked); - - Self::deposit_event(Event::AgreementRequestWithdrawn { - bucket_id, - provider, - payment_returned: total_locked, - }); - + Self::establish_replica_agreement_internal(&who, bucket_id, &provider, terms, &sig)?; Ok(()) } @@ -3372,19 +2993,13 @@ pub mod pallet { let public_key_bytes = provider_info.public_key.as_slice(); let account_id = match sig { sp_runtime::MultiSignature::Sr25519(_) | sp_runtime::MultiSignature::Ed25519(_) => { - ensure!( - public_key_bytes.len() == 32, - Error::::InvalidPublicKey - ); + ensure!(public_key_bytes.len() == 32, Error::::InvalidPublicKey); let mut key_bytes = [0u8; 32]; key_bytes.copy_from_slice(public_key_bytes); sp_runtime::AccountId32::new(key_bytes) } sp_runtime::MultiSignature::Ecdsa(_) | sp_runtime::MultiSignature::Eth(_) => { - ensure!( - public_key_bytes.len() == 33, - Error::::InvalidPublicKey - ); + ensure!(public_key_bytes.len() == 33, Error::::InvalidPublicKey); let hash = sp_io::hashing::blake2_256(public_key_bytes); sp_runtime::AccountId32::new(hash) } @@ -3760,28 +3375,6 @@ pub mod pallet { }); } - // Drain any still-pending AgreementRequests for this bucket and - // refund the requesters' locked funds. Without this the entries - // outlive the bucket and the provider's auto-coordinator keeps - // trying to accept them, every accept reverting with - // BucketNotFound and jamming the coordinator's queue. - for (provider, request) in AgreementRequests::::drain_prefix(bucket_id) { - let total_locked = if let Some(ref replica_params) = request.replica_params { - request - .payment_locked - .checked_add(&replica_params.sync_balance) - .unwrap_or(request.payment_locked) - } else { - request.payment_locked - }; - T::Currency::unreserve(&request.requester, total_locked); - Self::deposit_event(Event::AgreementRequestWithdrawn { - bucket_id, - provider, - payment_returned: total_locked, - }); - } - // Remove the bucket itself Buckets::::remove(bucket_id); @@ -4300,13 +3893,20 @@ pub mod pallet { /// with the specified account as admin. /// /// Parameters: - /// - `admin`: Account that will be the bucket admin - /// - `min_providers`: Minimum number of providers required + /// - `admin`: Account that will be the bucket admin. + /// - `min_providers`: Minimum number of primary providers required to + /// sign each checkpoint. + /// - `initial_primary`: Optional provider to seed as the bucket's + /// first `primary_providers` entry. Used by + /// `establish_storage_agreement_internal` to atomically create the + /// bucket together with its primary agreement; pass `None` for + /// buckets that will register primaries later. /// /// Returns: bucket_id pub fn create_bucket_internal( admin: &T::AccountId, min_providers: u32, + initial_primary: Option<&T::AccountId>, ) -> Result { let bucket_id = NextBucketId::::get(); NextBucketId::::put(bucket_id.saturating_add(1)); @@ -4321,11 +3921,18 @@ pub mod pallet { .try_push(admin_member) .map_err(|_| Error::::MaxMembersReached)?; + let mut primary_providers = BoundedVec::new(); + if let Some(p) = initial_primary { + primary_providers + .try_push(p.clone()) + .map_err(|_| Error::::MaxPrimaryProvidersReached)?; + } + let bucket = Bucket { members, frozen_start_seq: None, min_providers, - primary_providers: BoundedVec::new(), + primary_providers, snapshot: None, historical_roots: [(0, H256::zero()); 6], total_snapshots: 0, @@ -4367,10 +3974,7 @@ pub mod pallet { // Quote must not be stale. let current_block = frame_system::Pallet::::block_number(); - ensure!( - terms.valid_until >= current_block, - Error::::TermsExpired - ); + ensure!(terms.valid_until >= current_block, Error::::TermsExpired); // Provider lookup + signature check over blake2_256(SCALE(terms)). let provider_info = @@ -4426,39 +4030,9 @@ pub mod pallet { T::Currency::reserve(owner, payment)?; // Bucket creation folded in: owner is sole admin, provider is the - // bucket's single primary. - let bucket_id = NextBucketId::::get(); - NextBucketId::::put(bucket_id.saturating_add(1)); - - let admin_member = Member { - account: owner.clone(), - role: Role::Admin, - }; - let mut members = BoundedVec::new(); - members - .try_push(admin_member) - .map_err(|_| Error::::MaxMembersReached)?; - let mut primary_providers = BoundedVec::new(); - primary_providers - .try_push(provider.clone()) - .map_err(|_| Error::::MaxPrimaryProvidersReached)?; - - let bucket = Bucket { - members, - frozen_start_seq: None, - min_providers: 1, - primary_providers, - snapshot: None, - historical_roots: [(0, H256::zero()); 6], - total_snapshots: 0, - }; - Buckets::::insert(bucket_id, bucket); - - MemberBuckets::::try_mutate(owner, |buckets| { - buckets - .try_push(bucket_id) - .map_err(|_| Error::::TooManyBucketsForMember) - })?; + // bucket's single primary. `create_bucket_internal` emits + // `BucketCreated` for us. + let bucket_id = Self::create_bucket_internal(owner, 1, Some(provider))?; let expires_at = current_block.saturating_add(terms.duration); let agreement = StorageAgreement { @@ -4480,10 +4054,6 @@ pub mod pallet { }); StorageAgreements::::insert(bucket_id, provider, agreement); - Self::deposit_event(Event::BucketCreated { - bucket_id, - admin: owner.clone(), - }); Self::deposit_event(Event::StorageAgreementEstablished { bucket_id, provider: provider.clone(), @@ -4495,168 +4065,134 @@ pub mod pallet { Ok(bucket_id) } - /// Request a primary storage agreement internally (for use by other pallets). + /// Redeem provider-signed terms for a replica agreement (used directly + /// by the `establish_replica_agreement` extrinsic and by higher-layer + /// pallets that fold replica establishment into their own flows). /// - /// This creates a primary storage agreement without requiring admin origin check. - /// - /// Parameters: - /// - `owner`: Account that owns the agreement and will pay for it - /// - `bucket_id`: Target bucket - /// - `provider`: Provider to store data - /// - `max_bytes`: Maximum storage size - /// - `duration`: Storage duration in blocks - /// - `max_payment`: Maximum payment willing to pay - pub fn request_primary_agreement_internal( + /// Verifies the signature, advances the provider's replay window, then + /// runs the provider/capacity/stake checks before opening the replica + /// agreement on an existing bucket. `terms.replica_params` must be + /// `Some(_)`. + pub fn establish_replica_agreement_internal( owner: &T::AccountId, bucket_id: BucketId, provider: &T::AccountId, - max_bytes: u64, - duration: BlockNumberFor, - max_payment: BalanceOf, + terms: AgreementTermsOf, + sig: &sp_runtime::MultiSignature, ) -> DispatchResult { - let bucket = Buckets::::get(bucket_id).ok_or(Error::::BucketNotFound)?; - - // Check primary provider limit - ensure!( - bucket.primary_providers.len() < T::MaxPrimaryProviders::get() as usize, - Error::::MaxPrimaryProvidersReached - ); - - let provider_info = - Providers::::get(provider).ok_or(Error::::ProviderNotFound)?; - Self::ensure_provider_active(&provider_info)?; - - ensure!( - provider_info.settings.accepting_primary, - Error::::ProviderNotAcceptingPrimary - ); - - Self::validate_duration(&provider_info.settings, duration)?; - - let payment = Self::calculate_payment( - provider_info.settings.price_per_byte, - max_bytes, - duration, - )?; - ensure!(payment <= max_payment, Error::::PaymentExceedsMax); - - T::Currency::reserve(owner, payment)?; + // Origin must match the owner the provider signed for. + ensure!(&terms.owner == owner, Error::::TermsOwnerMismatch); + // Quote must not be stale. let current_block = frame_system::Pallet::::block_number(); - let expires_at = current_block.saturating_add(T::RequestTimeout::get()); - - let request = AgreementRequest { - requester: owner.clone(), - max_bytes, - payment_locked: payment, - duration, - expires_at, - replica_params: None, // Primary agreement - }; + ensure!(terms.valid_until >= current_block, Error::::TermsExpired); + // Target bucket must exist. ensure!( - !AgreementRequests::::contains_key(bucket_id, provider), - Error::::AgreementRequestAlreadyExists + Buckets::::contains_key(bucket_id), + Error::::BucketNotFound ); - AgreementRequests::::insert(bucket_id, provider, request); - - Self::deposit_event(Event::AgreementRequested { - bucket_id, - provider: provider.clone(), - requester: owner.clone(), - max_bytes, - payment_locked: payment, - duration, - }); - - Ok(()) - } - - /// Request a replica storage agreement internally (for use by other pallets). - /// - /// This creates a replica storage agreement without requiring origin check. - /// - /// Parameters: - /// - `owner`: Account that owns the agreement and will pay for it - /// - `bucket_id`: Target bucket - /// - `provider`: Provider to store replica - /// - `max_bytes`: Maximum storage size - /// - `duration`: Storage duration in blocks - /// - `max_payment`: Maximum payment willing to pay - /// - `sync_balance`: Balance reserved for sync operations - pub fn request_replica_agreement_internal( - owner: &T::AccountId, - bucket_id: BucketId, - provider: &T::AccountId, - max_bytes: u64, - duration: BlockNumberFor, - max_payment: BalanceOf, - sync_balance: BalanceOf, - ) -> DispatchResult { + // No existing agreement for (bucket, provider). ensure!( - Buckets::::contains_key(bucket_id), - Error::::BucketNotFound + !StorageAgreements::::contains_key(bucket_id, provider), + Error::::AgreementAlreadyExists ); + // Replica terms must be present for a replica agreement. + let replica_terms = terms + .replica_params + .as_ref() + .ok_or(Error::::MissingReplicaTerms)? + .clone(); + + // Provider lookup + signature check over blake2_256(SCALE(terms)). let provider_info = Providers::::get(provider).ok_or(Error::::ProviderNotFound)?; - Self::ensure_provider_active(&provider_info)?; + Self::verify_terms_signature(&provider_info, &terms, sig)?; + // Replay window: at most once per nonce, within the trailing 256 slots. + ProviderReplayState::::try_mutate(provider, |window| -> DispatchResult { + window.try_accept(terms.nonce).map_err(|e| match e { + ReplayError::AlreadyUsed => Error::::NonceAlreadyUsed, + ReplayError::TooOld => Error::::NonceTooOld, + })?; + Ok(()) + })?; + + // Validate on-chain provider's state. + Self::ensure_provider_active(&provider_info)?; + let sync_price = provider_info + .settings + .replica_sync_price + .ok_or(Error::::ProviderNotAcceptingReplicas)?; + Self::validate_duration(&provider_info.settings, terms.duration)?; ensure!( - provider_info.settings.replica_sync_price.is_some(), - Error::::ProviderNotAcceptingReplicas + provider_info.settings.price_per_byte <= terms.price_per_byte, + Error::::PriceExceedsMax ); - Self::validate_duration(&provider_info.settings, duration)?; + let new_committed = provider_info + .committed_bytes + .checked_add(terms.max_bytes) + .ok_or(Error::::ArithmeticOverflow)?; + if provider_info.settings.max_capacity > 0 { + ensure!( + new_committed <= provider_info.settings.max_capacity, + Error::::CapacityExceeded + ); + } - // Calculate payment - let payment = Self::calculate_payment( - provider_info.settings.price_per_byte, - max_bytes, - duration, - )?; - ensure!(payment <= max_payment, Error::::PaymentExceedsMax); + { + let bytes_as_balance: BalanceOf = new_committed.saturated_into(); + let required_stake = T::MinStakePerByte::get() + .checked_mul(&bytes_as_balance) + .ok_or(Error::::ArithmeticOverflow)?; + ensure!( + provider_info.stake >= required_stake, + Error::::InsufficientStakeForBytes + ); + } - // Total to lock = storage payment + sync balance + // Pay at the price the provider signed for, plus the sync balance. + let payment = + Self::calculate_payment(terms.price_per_byte, terms.max_bytes, terms.duration)?; let total_lock = payment - .checked_add(&sync_balance) + .checked_add(&replica_terms.sync_balance) .ok_or(Error::::ArithmeticOverflow)?; - - // Reserve funds T::Currency::reserve(owner, total_lock)?; - let current_block = frame_system::Pallet::::block_number(); - let expires_at = current_block.saturating_add(T::RequestTimeout::get()); - - let replica_params = ReplicaRequestParams { - sync_balance, - min_sync_interval: duration / 10u32.into(), // Sync every 10% of duration - }; - - let request = AgreementRequest { - requester: owner.clone(), - max_bytes, + let expires_at = current_block.saturating_add(terms.duration); + let agreement = StorageAgreement { + owner: owner.clone(), + max_bytes: terms.max_bytes, payment_locked: payment, - duration, + price_per_byte: terms.price_per_byte, expires_at, - replica_params: Some(replica_params), + extensions_blocked: false, + role: ProviderRole::Replica { + sync_balance: replica_terms.sync_balance, + sync_price, + min_sync_interval: replica_terms.min_sync_interval, + last_sync: None, + }, + started_at: current_block, }; - ensure!( - !AgreementRequests::::contains_key(bucket_id, provider), - Error::::AgreementRequestAlreadyExists - ); - - AgreementRequests::::insert(bucket_id, provider, request); + Providers::::mutate(provider, |maybe_provider| { + if let Some(p) = maybe_provider { + p.committed_bytes = p.committed_bytes.saturating_add(terms.max_bytes); + p.stats.agreements_total = p.stats.agreements_total.saturating_add(1); + } + }); + StorageAgreements::::insert(bucket_id, provider, agreement); - Self::deposit_event(Event::AgreementRequested { + Self::deposit_event(Event::ReplicaAgreementEstablished { bucket_id, provider: provider.clone(), - requester: owner.clone(), - max_bytes, - payment_locked: total_lock, - duration, + owner: owner.clone(), + terms, + expires_at, }); Ok(()) diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs index b1bf3cdd..f3a507fc 100644 --- a/primitives/src/agreement_term.rs +++ b/primitives/src/agreement_term.rs @@ -1,10 +1,11 @@ //! Provider-signed terms of a storage agreement. //! //! A provider quotes terms off-chain (e.g. over HTTP) and signs the SCALE -//! encoding of an `AgreementTerms` value. The owner then submits the signed -//! terms on-chain via `establish_storage_agreement`, which verifies the signature, -//! checks the replay window (see [`crate::provider_replay_state`]), and -//! creates the bucket + agreement atomically. +//! encoding of an `AgreementTerms` value. +//! +//! [`AgreementTerms`] shape covers both flavours: `replica` is +//! `None` for primary agreements and `Some(_)` for replica agreements, +//! carrying the per-sync funding parameters. use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::fmt::Debug; @@ -12,9 +13,6 @@ use scale_info::TypeInfo; /// Off-chain quote signed by the provider and redeemed on-chain by the owner. /// -/// Generic over the account/balance/block-number types so the same shape can -/// be reused by the pallet (with `BalanceOf`/`BlockNumberFor`), the -/// client SDK, and external tooling. #[derive( Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, )] @@ -34,4 +32,22 @@ pub struct AgreementTerms { /// Provider-chosen replay-protection nonce; uniqueness is enforced /// through the provider's sliding replay window. pub nonce: u64, + /// Replica-specific parameters. + /// - `None` means these are primary terms; + /// -`Some(_)` means the provider has quoted a replica agreement and the extra per-sync funding is included. + pub replica_params: Option>, +} + +/// Replica terms +/// +#[derive( + Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. The + /// pallet draws down `sync_price` from this on each accepted sync. + pub sync_balance: Balance, + /// Minimum blocks between sync confirmations the provider commits to. + pub min_sync_interval: BlockNumber, } diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 62d0452b..c6ae2d47 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -16,8 +16,8 @@ use sp_core::H256; pub mod agreement_term; pub mod provider_replay_state; -pub use agreement_term::AgreementTerms; -pub use provider_replay_state::{ReplayError, ReplayWindow, REPLAY_WINDOW_BITS}; +pub use agreement_term::*; +pub use provider_replay_state::*; /// Bucket ID is a stable, unique identifier (not an index into a collection). /// Using u64 ensures IDs never get reused even if buckets are deleted. @@ -141,18 +141,6 @@ pub enum RemovalReason { Expired, } -/// Parameters specific to replica agreement requests. -#[derive( - Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, -)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct ReplicaRequestParams { - /// Initial sync balance to fund per-sync payments - pub sync_balance: Balance, - /// Minimum blocks between sync confirmations. - pub min_sync_interval: BlockNumber, -} - // ───────────────────────────────────────────────────────────────────────────── // Challenge Types // ───────────────────────────────────────────────────────────────────────────── From 8ab179733188d2d0a5bc87c650a489ea556f6cef Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 16:46:12 +0700 Subject: [PATCH 05/44] test: update pallet tests for new agreement flow Update pallet/src/tests.rs to exercise the establish_storage_agreement / establish_replica_agreement extrinsics that replaced the legacy request/accept flow, and drop create_bucket / create_bucket_with_storage which are no longer exist. Test helpers: - Add sr25519 signing helpers (generate_provider_public_key, sign_terms) that use the runtime keystore registered in mock.rs. - Add primary_terms / replica_terms builders and a register_signing_provider helper for the common setup. And more test cases for new extrinsics & changes. --- pallet/src/lib.rs | 210 ----- pallet/src/tests.rs | 1999 ++++++++++++++++++++++++------------------- 2 files changed, 1097 insertions(+), 1112 deletions(-) diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 6f9e4610..02e4e0f9 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -1177,216 +1177,6 @@ pub mod pallet { // Bucket Management // ───────────────────────────────────────────────────────────────────── - /// Create a new bucket. - #[pallet::call_index(10)] - #[pallet::weight(T::WeightInfo::create_bucket())] - pub fn create_bucket(origin: OriginFor, min_providers: u32) -> DispatchResult { - let who = ensure_signed(origin)?; - - let bucket_id = NextBucketId::::get(); - NextBucketId::::put(bucket_id.saturating_add(1)); - - let admin_member = Member { - account: who.clone(), - role: Role::Admin, - }; - - let mut members = BoundedVec::new(); - members - .try_push(admin_member) - .map_err(|_| Error::::MaxMembersReached)?; - - let bucket = Bucket { - members, - frozen_start_seq: None, - min_providers, - primary_providers: BoundedVec::new(), - snapshot: None, - historical_roots: [(0, H256::zero()); 6], - total_snapshots: 0, - }; - - Buckets::::insert(bucket_id, bucket); - - // Update reverse index for creator - MemberBuckets::::try_mutate(&who, |buckets| { - buckets - .try_push(bucket_id) - .map_err(|_| Error::::TooManyBucketsForMember) - })?; - - Self::deposit_event(Event::BucketCreated { - bucket_id, - admin: who, - }); - - Ok(()) - } - - /// Create a new bucket with storage and open an agreement with an - /// explicitly chosen provider in one atomic operation. - /// - /// Callers discover providers off-chain via the `find_matching_providers` - /// runtime API, select one, and pass its account here. The pallet performs - /// an O(1) lookup of that single provider and re-validates every constraint - /// (active, accepting primary, duration, price, capacity, stake) before - /// opening the agreement — discovery is advisory, this is the source of truth. - /// - /// Providers who set `accepting_primary: true` have pre-consented to accepting - /// agreements within their stated parameters (capacity, price, duration). - #[pallet::call_index(16)] - #[pallet::weight(T::WeightInfo::create_bucket_with_storage())] - pub fn create_bucket_with_storage( - origin: OriginFor, - provider: T::AccountId, - max_bytes: u64, - duration: BlockNumberFor, - max_price_per_byte: BalanceOf, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - // O(1) lookup of the caller-selected provider, then re-validate every - // constraint against current chain state. - let provider_info = - Providers::::get(&provider).ok_or(Error::::ProviderNotFound)?; - - // Must not be mid-deregistration. - Self::ensure_provider_active(&provider_info)?; - - // Must be accepting primary agreements. - ensure!( - provider_info.settings.accepting_primary, - Error::::ProviderNotAcceptingPrimary - ); - - // Requested duration must be within the provider's accepted range. - Self::validate_duration(&provider_info.settings, duration)?; - - // Provider's price must not exceed the caller's ceiling. - ensure!( - provider_info.settings.price_per_byte <= max_price_per_byte, - Error::::PriceExceedsMax - ); - - // Provider must have enough free capacity for the requested bytes. - let new_committed = provider_info - .committed_bytes - .checked_add(max_bytes) - .ok_or(Error::::ArithmeticOverflow)?; - if provider_info.settings.max_capacity > 0 { - ensure!( - new_committed <= provider_info.settings.max_capacity, - Error::::CapacityExceeded - ); - } - - // Provider must hold enough stake to back the new total commitment. - { - let bytes_as_balance: BalanceOf = new_committed.saturated_into(); - let required_stake = T::MinStakePerByte::get() - .checked_mul(&bytes_as_balance) - .ok_or(Error::::ArithmeticOverflow)?; - ensure!( - provider_info.stake >= required_stake, - Error::::InsufficientStakeForBytes - ); - } - - // Calculate payment using provider's actual price - let payment = Self::calculate_payment( - provider_info.settings.price_per_byte, - max_bytes, - duration, - )?; - - // Reserve funds from caller - T::Currency::reserve(&who, payment)?; - - // Create the bucket - let bucket_id = NextBucketId::::get(); - NextBucketId::::put(bucket_id.saturating_add(1)); - - let admin_member = Member { - account: who.clone(), - role: Role::Admin, - }; - - let mut members = BoundedVec::new(); - members - .try_push(admin_member) - .map_err(|_| Error::::MaxMembersReached)?; - - let mut primary_providers = BoundedVec::new(); - primary_providers - .try_push(provider.clone()) - .map_err(|_| Error::::MaxPrimaryProvidersReached)?; - - let bucket = Bucket { - members, - frozen_start_seq: None, - min_providers: 1, - primary_providers, - snapshot: None, - historical_roots: [(0, H256::zero()); 6], - total_snapshots: 0, - }; - - Buckets::::insert(bucket_id, bucket); - - // Update reverse index for creator - MemberBuckets::::try_mutate(&who, |buckets| { - buckets - .try_push(bucket_id) - .map_err(|_| Error::::TooManyBucketsForMember) - })?; - - // Create the agreement - let current_block = frame_system::Pallet::::block_number(); - let expires_at = current_block.saturating_add(duration); - - let agreement = StorageAgreement { - owner: who.clone(), - max_bytes, - payment_locked: payment, - price_per_byte: provider_info.settings.price_per_byte, - expires_at, - extensions_blocked: false, - role: ProviderRole::Primary, - started_at: current_block, - }; - - // Update provider's committed_bytes - Providers::::mutate(&provider, |maybe_provider| { - if let Some(provider_info) = maybe_provider { - provider_info.committed_bytes = - provider_info.committed_bytes.saturating_add(max_bytes); - provider_info.stats.agreements_total = - provider_info.stats.agreements_total.saturating_add(1); - } - }); - - StorageAgreements::::insert(bucket_id, &provider, agreement); - - // Emit events - Self::deposit_event(Event::BucketCreated { - bucket_id, - admin: who.clone(), - }); - - Self::deposit_event(Event::AgreementAccepted { - bucket_id, - provider: provider.clone(), - expires_at, - }); - - Self::deposit_event(Event::ProviderAddedToBucket { - bucket_id, - provider, - }); - - Ok(()) - } - /// Redeem provider-signed terms: create a bucket + primary agreement /// in a single call. /// diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index f713a545..a3e8717c 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -1,14 +1,134 @@ //! Tests for the storage provider pallet. use crate::{mock::*, *}; +use codec::Encode; use frame_support::{assert_noop, assert_ok}; -use storage_primitives::{ProviderRole, Role}; +use sp_core::crypto::KeyTypeId; +use storage_primitives::{ + AgreementTerms, BucketId, ProviderRole, ReplicaTerms, Role, REPLAY_WINDOW_BITS, +}; -/// Helper function to create a test public key (32 bytes). +/// Key type used by the keystore in tests for provider signing material. +const PROVIDER_KEY_TYPE: KeyTypeId = KeyTypeId(*b"prov"); + +/// Static test public key for tests that never exercise signature verification +/// (e.g. provider register/settings flows). fn test_public_key() -> frame_support::BoundedVec> { vec![1u8; 32].try_into().unwrap() } +/// Generate a provider sr25519 keypair inside the runtime keystore. +/// +/// The returned public key bytes are what should be stored in +/// `register_provider`'s `public_key` argument so the pallet can verify +/// signatures produced by [`sign_terms`]. +fn generate_provider_public_key( + seed: &str, +) -> ( + sp_core::sr25519::Public, + frame_support::BoundedVec>, +) { + let public = sp_io::crypto::sr25519_generate(PROVIDER_KEY_TYPE, Some(seed.as_bytes().to_vec())); + let bounded = public.0.to_vec().try_into().unwrap(); + (public, bounded) +} + +/// Sign agreement terms with the provider's keystore key. +fn sign_terms( + public: &sp_core::sr25519::Public, + terms: &AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&terms.encode()); + let sig = sp_io::crypto::sr25519_sign(PROVIDER_KEY_TYPE, public, &hash) + .expect("keystore should sign with a key it generated"); + sp_runtime::MultiSignature::Sr25519(sig) +} + +/// Build [`AgreementTermsOf`] for a primary agreement (no replica params). +fn primary_terms( + owner: u64, + max_bytes: u64, + duration: u64, + price_per_byte: u64, + valid_until: u64, + nonce: u64, +) -> AgreementTermsOf { + AgreementTerms { + owner, + max_bytes, + duration, + price_per_byte, + valid_until, + nonce, + replica_params: None, + } +} + +/// Build [`AgreementTermsOf`] for a replica agreement. +fn replica_terms( + owner: u64, + max_bytes: u64, + duration: u64, + price_per_byte: u64, + valid_until: u64, + nonce: u64, + sync_balance: u64, + min_sync_interval: u64, +) -> AgreementTermsOf { + AgreementTerms { + owner, + max_bytes, + duration, + price_per_byte, + valid_until, + nonce, + replica_params: Some(ReplicaTerms { + sync_balance, + min_sync_interval, + }), + } +} + +/// Register a provider and apply common settings used by establish-agreement +/// tests. Returns the sr25519 public key the provider was registered with. +fn register_signing_provider( + provider: u64, + seed: &str, + stake: u64, + settings: ProviderSettings, +) -> sp_core::sr25519::Public { + let multiaddr = format!("/ip4/127.0.0.1/tcp/300{provider}"); + let (public, bounded) = generate_provider_public_key(seed); + assert_ok!(StorageProvider::register_provider( + RuntimeOrigin::signed(provider), + multiaddr.as_bytes().to_vec().try_into().unwrap(), + bounded, + stake, + )); + assert_ok!(StorageProvider::update_provider_settings( + RuntimeOrigin::signed(provider), + settings, + )); + public +} + +/// Sensible default provider settings for tests that just need to accept +/// primaries/replicas. +fn default_test_settings( + price_per_byte: u64, + replica_sync_price: Option, +) -> ProviderSettings { + ProviderSettings { + min_duration: 10, + max_duration: 1000, + price_per_byte, + accepting_primary: true, + replica_sync_price, + accepting_extensions: true, + max_capacity: 0, + } +} + mod provider_tests { use super::*; @@ -104,14 +224,41 @@ mod provider_tests { #[test] fn deregister_provider_full_flow_announce_then_complete() { new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(1, "//Provider", 200, settings); + + // Open a primary agreement so the provider lifecycle isn't + // trivial: deregister must wait for committed_bytes to drop + // back to zero, which only happens once the agreement is + // settled (via claim_expired_agreement after expiry). + let terms = primary_terms(2, 50, 100, 0, 10_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(2), + 1, + terms, + sig, + )); + assert_eq!(Providers::::get(1).unwrap().committed_bytes, 50); + let bucket_id = NextBucketId::::get() - 1; + let agreement = StorageAgreements::::get(bucket_id, 1).unwrap(); - assert_ok!(StorageProvider::register_provider( + // While the agreement is live, deregister must refuse to + // even start the announcement. + assert_noop!( + StorageProvider::deregister_provider(RuntimeOrigin::signed(1)), + Error::::ProviderHasActiveAgreements + ); + + // Wait past expires_at + SettlementTimeout so the provider + // can claim payment and release its committed_bytes. + run_to_block(agreement.expires_at + 51); + assert_ok!(StorageProvider::claim_expired_agreement( RuntimeOrigin::signed(1), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 + bucket_id, )); + assert_eq!(Providers::::get(1).unwrap().committed_bytes, 0); let balance_before = Balances::free_balance(1); @@ -271,137 +418,30 @@ mod provider_tests { } #[test] - fn withdraw_agreement_request_still_works_during_announcement() { - // Defensive: if a request was created BEFORE announce and the - // provider is now exiting, the owner must still be able to recover - // their locked funds via withdraw_agreement_request. Otherwise the - // owner's payment would be stuck until the request expires - // (RequestTimeout) and even then there's no automatic refund path. - new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - - // Owner creates request → payment locked. - assert_ok!(StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(1), - 0, - 2, - 50, - 100, - 1000 - )); - - // Provider announces deregister (without accepting). - // committed_bytes is 0 because the request was never accepted. - assert_ok!(StorageProvider::deregister_provider(RuntimeOrigin::signed( - 2 - ))); - - // Owner can still withdraw their pending request. - assert_ok!(StorageProvider::withdraw_agreement_request( - RuntimeOrigin::signed(1), - 0, - 2, - )); - assert!(AgreementRequests::::get(0, 2).is_none()); - }); - } - - #[test] - fn agreement_entry_points_reject_deregistering_provider() { + fn establish_storage_agreement_rejects_due_to_deregistering_provider() { + // Once a provider has announced deregistration, + // `establish_storage_agreement` (and `establish_replica_agreement`) + // must reject otherwise-valid signed terms with `DeregisterAnnounced`. new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - - // Set up an existing agreement so top_up_agreement has something - // to top up — this happens BEFORE announce. - assert_ok!(StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(1), - 0, - 2, - 50, - 100, - 1000 - )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 - )); - - // End the agreement so committed_bytes drops back to 0 and - // announce is allowed. Wait past expires_at + SettlementTimeout - // so claim_expired_agreement succeeds. - run_to_block(200); - assert_ok!(StorageProvider::claim_expired_agreement( - RuntimeOrigin::signed(2), - 0 - )); + System::set_block_number(1); + let settings = default_test_settings(0, Some(1)); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Now announce. assert_ok!(StorageProvider::deregister_provider(RuntimeOrigin::signed( 2 ))); - // Every agreement-creating entry point now rejects with - // DeregisterAnnounced. - assert_noop!( - StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(1), - 0, - 2, - 50, - 100, - 1000 - ), - Error::::DeregisterAnnounced - ); + let terms = primary_terms(1, 50, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); assert_noop!( - StorageProvider::request_agreement( + StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 50, - 100, - 1000, - storage_primitives::ReplicaRequestParams { - sync_balance: 0, - min_sync_interval: 10, - } + terms, + sig, ), Error::::DeregisterAnnounced ); - - // accept_agreement: there's no pending request now, but if there - // were, the deregister check would fire before the request - // lookup. We simulate by inserting a dummy request via storage. - crate::AgreementRequests::::insert( - 0, - 2, - crate::AgreementRequest { - requester: 1, - max_bytes: 50, - payment_locked: 0, - duration: 100, - expires_at: 10_000, - replica_params: None, - }, - ); - assert_noop!( - StorageProvider::accept_agreement(RuntimeOrigin::signed(2), 0), - Error::::DeregisterAnnounced - ); }); } @@ -448,32 +488,21 @@ mod provider_tests { #[test] fn deregister_provider_fails_with_active_agreements() { new_test_ext().execute_with(|| { - // Setup provider and bucket - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - - // Create agreement (max_bytes = 100 fits within stake of 200) - // payment = price_per_byte(0) * max_bytes * duration = 0 - assert_ok!(StorageProvider::request_primary_agreement( + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + + // Open a primary agreement via the signed-terms flow so the + // provider has live committed bytes. + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 100, - 100, - 1000 - )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 + terms, + sig, )); - // Try to deregister assert_noop!( StorageProvider::deregister_provider(RuntimeOrigin::signed(2)), Error::::ProviderHasActiveAgreements @@ -552,31 +581,20 @@ mod provider_tests { #[test] fn update_provider_settings_fails_with_capacity_below_committed() { new_test_ext().execute_with(|| { - // Setup provider and bucket - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - - // Create agreement for 100 bytes - assert_ok!(StorageProvider::request_primary_agreement( + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + + // Open a 100-byte primary agreement so committed_bytes = 100. + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 100, // max_bytes - 100, - 1000 - )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 + terms, + sig, )); - // Try to set max_capacity below committed_bytes let new_settings = ProviderSettings { min_duration: 10u64, max_duration: 1000u64, @@ -719,41 +737,34 @@ mod provider_tests { #[test] fn set_extensions_blocked_works_on_active_agreement() { new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - assert_ok!(StorageProvider::request_primary_agreement( + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 100, - 100, - 1000 - )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 + terms, + sig, )); + let bucket_id = NextBucketId::::get() - 1; assert_ok!(StorageProvider::set_extensions_blocked( RuntimeOrigin::signed(2), - 0, - true + bucket_id, + true, )); - let agreement = StorageAgreements::::get(0, 2).unwrap(); + let agreement = StorageAgreements::::get(bucket_id, 2).unwrap(); assert!(agreement.extensions_blocked); assert_ok!(StorageProvider::set_extensions_blocked( RuntimeOrigin::signed(2), - 0, - false + bucket_id, + false, )); - let agreement = StorageAgreements::::get(0, 2).unwrap(); + let agreement = StorageAgreements::::get(bucket_id, 2).unwrap(); assert!(!agreement.extensions_blocked); }); } @@ -761,59 +772,42 @@ mod provider_tests { #[test] fn set_extensions_blocked_fails_after_agreement_expires() { new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - assert_ok!(StorageProvider::request_primary_agreement( + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 100, - 100, // duration = 100 → expires_at = current_block + 100 - 1000 + terms, + sig, )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 - )); - - let agreement = StorageAgreements::::get(0, 2).unwrap(); + let bucket_id = NextBucketId::::get() - 1; + let agreement = StorageAgreements::::get(bucket_id, 2).unwrap(); // At expires_at exactly, the agreement is no longer extendable // (strict `<` in the pallet guard). run_to_block(agreement.expires_at); assert_noop!( - StorageProvider::set_extensions_blocked(RuntimeOrigin::signed(2), 0, true), + StorageProvider::set_extensions_blocked(RuntimeOrigin::signed(2), bucket_id, true), Error::::AgreementExpired ); // Past expiry, same rejection. run_to_block(agreement.expires_at + 1); assert_noop!( - StorageProvider::set_extensions_blocked(RuntimeOrigin::signed(2), 0, true), + StorageProvider::set_extensions_blocked(RuntimeOrigin::signed(2), bucket_id, true), Error::::AgreementExpired ); }); } #[test] - fn accept_agreement_fails_when_capacity_exceeded() { + fn establish_storage_agreement_fails_when_capacity_exceeded() { new_test_ext().execute_with(|| { - // Setup provider with limited capacity - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - // Set max_capacity to 50 bytes (stake of 200 can back this) + System::set_block_number(1); let settings = ProviderSettings { min_duration: 0u64, max_duration: 1000u64, @@ -823,66 +817,45 @@ mod provider_tests { accepting_extensions: true, max_capacity: 50, }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - // Create bucket - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Request agreement for 60 bytes (exceeds max_capacity of 50) - // payment = price_per_byte * max_bytes * duration = 1 * 60 * 10 = 600 - assert_ok!(StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(1), - 0, - 2, - 60, // Exceeds max_capacity of 50 - 10, - 600 - )); - - // Accept should fail due to capacity exceeded + // 60 bytes exceeds the provider's 50-byte cap. + let terms = primary_terms(1, 60, 10, 1, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); assert_noop!( - StorageProvider::accept_agreement(RuntimeOrigin::signed(2), 0), + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), Error::::CapacityExceeded ); }); } #[test] - fn accept_agreement_works_with_unlimited_capacity() { + fn establish_storage_agreement_works_with_unlimited_capacity() { new_test_ext().execute_with(|| { - // Setup provider with unlimited capacity (max_capacity = 0) - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - // Settings with unlimited capacity (default) - // Default max_capacity is 0 which means unlimited - - // Create bucket - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + let settings = ProviderSettings { + min_duration: 0u64, + max_duration: 1000u64, + price_per_byte: 1u64, + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 0, // Unlimited + }; + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Request agreement for 100 bytes - // payment = 1 * 100 * 10 = 1000 - assert_ok!(StorageProvider::request_primary_agreement( + let terms = primary_terms(1, 100, 10, 1, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 100, - 10, - 1000 - )); - - // Accept should succeed (capacity is unlimited, stake of 200 covers 100 bytes) - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 + terms, + sig, )); let provider = Providers::::get(2).unwrap(); @@ -891,18 +864,9 @@ mod provider_tests { } #[test] - fn accept_agreement_works_within_capacity() { + fn establish_storage_agreement_works_within_capacity() { new_test_ext().execute_with(|| { - // Setup provider with limited capacity - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - // Set max_capacity to 150 bytes (stake of 200 covers this) + System::set_block_number(1); let settings = ProviderSettings { min_duration: 0u64, max_duration: 1000u64, @@ -912,29 +876,15 @@ mod provider_tests { accepting_extensions: true, max_capacity: 150, }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - // Create bucket - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Request agreement for 100 bytes (within capacity) - // payment = 1 * 100 * 10 = 1000 - assert_ok!(StorageProvider::request_primary_agreement( + let terms = primary_terms(1, 100, 10, 1, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - 100, - 10, - 1000 - )); - - // Accept should succeed - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 + terms, + sig, )); let provider = Providers::::get(2).unwrap(); @@ -944,505 +894,1049 @@ mod provider_tests { } } -mod bucket_tests { +mod establish_storage_agreement_tests { use super::*; + /// Happy path: signed terms produce a bucket + primary agreement + /// atomically, the provider's `committed_bytes` advances, and + /// `ProviderReplayState` records the nonce. #[test] - fn create_bucket_works() { + fn establishes_bucket_and_primary_agreement() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 2)); + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.min_providers, 2); - assert_eq!(bucket.members.len(), 1); + let owner_balance_before = Balances::free_balance(1); + let terms = primary_terms(1, 100, 100, 0, 1_000, 7); + let sig = sign_terms(&provider_pk, &terms); + + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms.clone(), + sig, + )); + + // Bucket created with owner as sole admin and provider as primary. + let bucket_id = NextBucketId::::get() - 1; + let bucket = Buckets::::get(bucket_id).unwrap(); + assert_eq!(bucket.primary_providers.to_vec(), vec![2]); assert_eq!(bucket.members[0].account, 1); assert_eq!(bucket.members[0].role, Role::Admin); - assert!(bucket.snapshot.is_none()); - assert!(bucket.frozen_start_seq.is_none()); - // Check bucket ID incremented - assert_eq!(NextBucketId::::get(), 1); - }); - } + // Primary agreement opened, with terms reflected in storage. + let agreement = StorageAgreements::::get(bucket_id, 2).unwrap(); + assert_eq!(agreement.owner, 1); + assert_eq!(agreement.max_bytes, 100); + assert!(matches!(agreement.role, ProviderRole::Primary)); - #[test] - fn create_multiple_buckets_increments_id() { - new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(2), 2)); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 3)); + // Provider commitments updated. + let provider = Providers::::get(2).unwrap(); + assert_eq!(provider.committed_bytes, 100); + assert_eq!(provider.stats.agreements_total, 1); + + // No payment since price_per_byte = 0. + assert_eq!(Balances::free_balance(1), owner_balance_before); - assert_eq!(NextBucketId::::get(), 3); - assert!(Buckets::::get(0).is_some()); - assert!(Buckets::::get(1).is_some()); - assert!(Buckets::::get(2).is_some()); + // Replay window now anchored at nonce 7. + let window = ProviderReplayState::::get(2); + assert_eq!(window.hwm, 7); + assert_eq!(window.bitmap[0] & 1, 1); }); } #[test] - fn set_member_works() { + fn reserves_payment_at_signed_price() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + // Provider signs terms at price 1; pallet should reserve that + // amount even if the on-chain advertised price later drops. + let settings = ProviderSettings { + min_duration: 0u64, + max_duration: 1000u64, + price_per_byte: 1u64, + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 0, + }; + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Add writer - assert_ok!(StorageProvider::set_member( + let before = Balances::free_balance(1); + // payment = 1 * 100 * 10 = 1000 + let terms = primary_terms(1, 100, 10, 1, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - Role::Writer + terms, + sig, )); + assert_eq!(Balances::free_balance(1), before - 1000); + }); + } - let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.members.len(), 2); + #[test] + fn rejects_when_terms_owner_does_not_match_origin() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - let writer = bucket.members.iter().find(|m| m.account == 2).unwrap(); - assert_eq!(writer.role, Role::Writer); + // Terms signed for owner = 1, but origin = 3. + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(3), + 2, + terms, + sig, + ), + Error::::TermsOwnerMismatch + ); }); } #[test] - fn set_member_updates_existing_role() { + fn rejects_expired_terms() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(50); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Add as writer - assert_ok!(StorageProvider::set_member( - RuntimeOrigin::signed(1), - 0, - 2, - Role::Writer - )); - - // Promote to admin - assert_ok!(StorageProvider::set_member( - RuntimeOrigin::signed(1), - 0, - 2, - Role::Admin - )); + // valid_until is in the past. + let terms = primary_terms(1, 100, 100, 0, 10, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::TermsExpired + ); + }); + } - let bucket = Buckets::::get(0).unwrap(); - let member = bucket.members.iter().find(|m| m.account == 2).unwrap(); - assert_eq!(member.role, Role::Admin); + #[test] + fn rejects_signature_from_wrong_signer() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let _provider_pk = register_signing_provider(2, "//Provider", 200, settings.clone()); + // A second, unrelated keypair the pallet has never heard of. + let (other_pk, _) = generate_provider_public_key("//Imposter"); + + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&other_pk, &terms); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::InvalidProviderSignature + ); }); } #[test] - fn set_member_fails_for_non_admin() { + fn rejects_tampered_terms() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + + // Sign one set of terms, then submit a different set with the + // same signature: signature won't verify over the new encoding. + let original = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &original); + + let mut tampered = original.clone(); + tampered.max_bytes = 999; - // Non-admin tries to add member assert_noop!( - StorageProvider::set_member(RuntimeOrigin::signed(2), 0, 3, Role::Writer), - Error::::NotBucketAdmin + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + tampered, + sig, + ), + Error::::InvalidProviderSignature ); }); } #[test] - fn cannot_demote_other_admin() { + fn rejects_unregistered_provider() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + // Generate a key but never register the provider. + let (provider_pk, _) = generate_provider_public_key("//Ghost"); + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::ProviderNotFound + ); + }); + } - // Add second admin - assert_ok!(StorageProvider::set_member( - RuntimeOrigin::signed(1), - 0, - 2, - Role::Admin - )); + #[test] + fn rejects_provider_not_accepting_primary() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let mut settings = default_test_settings(0, None); + settings.accepting_primary = false; + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Admin 1 tries to demote admin 2 + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); assert_noop!( - StorageProvider::set_member(RuntimeOrigin::signed(1), 0, 2, Role::Writer), - Error::::CannotDemoteAdmin + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::ProviderNotAcceptingPrimary ); }); } #[test] - fn last_admin_cannot_self_demote() { + fn rejects_duration_below_provider_minimum() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + let settings = ProviderSettings { + min_duration: 500, + max_duration: 1000, + price_per_byte: 0, + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 0, + }; + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - // Admin 1 is the sole admin and cannot demote themselves. + let terms = primary_terms(1, 100, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); assert_noop!( - StorageProvider::set_member(RuntimeOrigin::signed(1), 0, 1, Role::Writer), - Error::::LastAdminCannotBeRemoved + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::DurationTooShort ); }); } #[test] - fn last_admin_cannot_be_removed() { + fn rejects_when_signed_price_below_on_chain_price() { + // If a provider raises their on-chain price after signing, the + // pallet enforces `provider_info.price_per_byte <= terms.price_per_byte` + // and rejects with `PriceExceedsMax`. new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + let settings = ProviderSettings { + min_duration: 0, + max_duration: 1000, + price_per_byte: 5, // Current on-chain price. + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 0, + }; + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + // Signed terms quote a stale, lower price. + let terms = primary_terms(1, 10, 10, 1, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); assert_noop!( - StorageProvider::remove_member(RuntimeOrigin::signed(1), 0, 1), - Error::::LastAdminCannotBeRemoved + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::PriceExceedsMax ); }); } #[test] - fn admin_can_demote_self() { + fn rejects_when_stake_insufficient_for_committed_bytes() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + // MinStakePerByte = 1, stake = 100 → can only back 100 bytes. + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 100, settings); + + // 200 bytes requires 200 stake; provider only has 100. + let terms = primary_terms(1, 200, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::InsufficientStakeForBytes + ); + }); + } - // Add second admin - assert_ok!(StorageProvider::set_member( - RuntimeOrigin::signed(1), - 0, - 2, - Role::Admin - )); + #[test] + fn rejects_replayed_nonce_in_window() { + // Same nonce twice — the second submission lands inside the window + // with its bit already set. + new_test_ext().execute_with(|| { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 1_000, settings); - // Admin 1 demotes self - assert_ok!(StorageProvider::set_member( + let terms = primary_terms(1, 10, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, - 1, - Role::Writer + 2, + terms.clone(), + sig.clone(), )); - - let bucket = Buckets::::get(0).unwrap(); - let member = bucket.members.iter().find(|m| m.account == 1).unwrap(); - assert_eq!(member.role, Role::Writer); + // Same nonce, same terms → AlreadyUsed. + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::NonceAlreadyUsed + ); }); } #[test] - fn remove_member_works() { + fn accepts_nonce_at_window_edge_and_rejects_one_past() { + // After advancing hwm to 300, nonce 45 (distance 255) is still in + // the window, but nonce 44 (distance 256) is one slot past it. new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - assert_ok!(StorageProvider::set_member( + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 5_000, settings); + + let advance = primary_terms(1, 1, 100, 0, 1_000, 300); + let sig = sign_terms(&provider_pk, &advance); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, 2, - Role::Writer + advance, + sig, )); + assert_eq!(ProviderReplayState::::get(2).hwm, 300); - assert_ok!(StorageProvider::remove_member( + // Distance == REPLAY_WINDOW_BITS - 1 ⇒ accepted. + let edge_nonce = 300 - (REPLAY_WINDOW_BITS as u64 - 1); + let at_edge = primary_terms(1, 1, 100, 0, 1_000, edge_nonce); + let sig = sign_terms(&provider_pk, &at_edge); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, - 2 + 2, + at_edge, + sig, )); - let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.members.len(), 1); - assert!(!bucket.members.iter().any(|m| m.account == 2)); + // Distance == REPLAY_WINDOW_BITS ⇒ rejected. + let past_edge_nonce = 300 - REPLAY_WINDOW_BITS as u64; + let past_edge = primary_terms(1, 1, 100, 0, 1_000, past_edge_nonce); + let sig = sign_terms(&provider_pk, &past_edge); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + past_edge, + sig, + ), + Error::::NonceTooOld + ); }); } #[test] - fn remove_member_fails_for_non_existent() { + fn rejects_nonce_far_below_window() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 5_000, settings); + + let advance = primary_terms(1, 1, 100, 0, 1_000, 100_000); + let sig = sign_terms(&provider_pk, &advance); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + advance, + sig, + )); + let ancient = primary_terms(1, 1, 100, 0, 1_000, 5); + let sig = sign_terms(&provider_pk, &ancient); assert_noop!( - StorageProvider::remove_member(RuntimeOrigin::signed(1), 0, 99), - Error::::MemberNotFound + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + ancient, + sig, + ), + Error::::NonceTooOld ); }); } #[test] - fn set_min_providers_works() { + fn accepts_out_of_order_nonces() { + // Quoting concurrency: nonces issued out of order should all be + // accepted as long as none are replayed. new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 2)); + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 5_000, settings); + + for nonce in [3u64, 7, 1, 10, 2] { + let terms = primary_terms(1, 1, 100, 0, 1_000, nonce); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + )); + } - // Can set to 0 (no minimum) - assert_ok!(StorageProvider::set_min_providers( + // hwm follows the max nonce seen. + let window = ProviderReplayState::::get(2); + assert_eq!(window.hwm, 10); + + // Replays of any of those nonces are rejected. + for nonce in [3u64, 7, 1, 10, 2] { + let terms = primary_terms(1, 1, 100, 0, 1_000, nonce); + let sig = sign_terms(&provider_pk, &terms); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::NonceAlreadyUsed + ); + } + }); + } + + #[test] + fn forward_jump_beyond_window_clears_old_bits() { + // Bitmap shift: when hwm jumps forward by >= REPLAY_WINDOW_BITS, + // every previously-set bit drops off the window so prior nonces + // are now NonceTooOld, not NonceAlreadyUsed. + new_test_ext().execute_with(|| { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 5_000, settings); + + for nonce in [1u64, 2, 50] { + let terms = primary_terms(1, 1, 100, 0, 1_000, nonce); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + )); + } + // Jump forward by >> REPLAY_WINDOW_BITS so the bitmap is fully cleared. + let jump = primary_terms(1, 1, 100, 0, 1_000, 10_000); + let sig = sign_terms(&provider_pk, &jump); + assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), - 0, - 0 + 2, + jump, + sig, )); - let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.min_providers, 0); + let window = ProviderReplayState::::get(2); + assert_eq!(window.hwm, 10_000); + // Only the new hwm bit is set; everything else is zero. + assert_eq!(window.bitmap[0], 0b0000_0001); + for byte in &window.bitmap[1..] { + assert_eq!(*byte, 0); + } + + // The previously-used nonces now report TooOld, proving the + // bitmap shifted them out (not AlreadyUsed). + for nonce in [1u64, 2, 50] { + let terms = primary_terms(1, 1, 100, 0, 1_000, nonce); + let sig = sign_terms(&provider_pk, &terms); + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::NonceTooOld + ); + } }); } #[test] - fn freeze_bucket_requires_snapshot() { + fn max_primary_providers_enforced_via_establish() { + // `establish_storage_agreement` always creates a fresh single-primary + // bucket, so the limit is exercised at bucket creation: 5 buckets + // succeed, the 6th still succeeds (each is independent). To test the + // primary-cap path, we use `establish_replica_agreement` over the + // same bucket — but that exercises a different code path. + // Here we just confirm 5 sequential establishments produce 5 buckets, + // each with their own primary. new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + System::set_block_number(1); + for i in 2..=6u64 { + let settings = default_test_settings(0, None); + let seed = format!("//Provider{i}"); + let provider_pk = register_signing_provider(i, &seed, 200, settings); + let terms = primary_terms(1, 10, 100, 0, 1_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + i, + terms, + sig, + )); + } + // Five buckets, one per provider. + assert_eq!(NextBucketId::::get(), 5); + }); + } + #[test] + fn rejects_duplicate_agreement_when_nonce_reused_across_owners() { + // A provider can't double-sell the same nonce, even to a different owner. + new_test_ext().execute_with(|| { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 5_000, settings); + + let terms1 = primary_terms(1, 10, 100, 0, 1_000, 42); + let sig1 = sign_terms(&provider_pk, &terms1); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms1, + sig1, + )); + + // Provider signs new terms for a different owner but reuses nonce 42. + let terms2 = primary_terms(3, 10, 100, 0, 1_000, 42); + let sig2 = sign_terms(&provider_pk, &terms2); assert_noop!( - StorageProvider::freeze_bucket(RuntimeOrigin::signed(1), 0), - Error::::NoSnapshot + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(3), + 2, + terms2, + sig2, + ), + Error::::NonceAlreadyUsed ); }); } } -mod agreement_tests { +mod establish_replica_agreement_tests { use super::*; - fn setup_provider_and_bucket() { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 + /// Set up a primary bucket via `establish_storage_agreement` and return + /// `(bucket_id, primary_provider_pk)`. The bucket id is used for the + /// subsequent replica agreement. + fn setup_primary_bucket() -> (BucketId, sp_core::sr25519::Public) { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Primary", 1_000, settings); + let terms = primary_terms(1, 100, 100, 0, 10_000, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + let bucket_id = NextBucketId::::get() - 1; + (bucket_id, provider_pk) + } + + /// Register a replica-accepting provider and return its keypair. + fn register_replica_provider(account: u64, seed: &str) -> sp_core::sr25519::Public { + let settings = default_test_settings(0, Some(1)); + register_signing_provider(account, seed, 1_000, settings) } #[test] - fn request_primary_agreement_works() { + fn establishes_replica_agreement() { new_test_ext().execute_with(|| { - setup_provider_and_bucket(); - - assert_ok!(StorageProvider::request_primary_agreement( + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); + + let owner_balance_before = Balances::free_balance(1); + // payment = price 0 * 50 * 100 = 0; sync_balance = 25 is reserved. + let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_ok!(StorageProvider::establish_replica_agreement( RuntimeOrigin::signed(1), - 0, // bucket_id - 2, // provider - 1000, // max_bytes - 100, // duration - 1000 // max_payment + bucket_id, + 3, + terms, + sig, )); - let request = AgreementRequests::::get(0, 2).unwrap(); - assert_eq!(request.requester, 1); - assert_eq!(request.max_bytes, 1000); - assert_eq!(request.duration, 100); - assert!(request.replica_params.is_none()); + let agreement = StorageAgreements::::get(bucket_id, 3).unwrap(); + assert_eq!(agreement.owner, 1); + assert_eq!(agreement.max_bytes, 50); + match agreement.role { + ProviderRole::Replica { + sync_balance, + sync_price, + min_sync_interval, + last_sync, + } => { + assert_eq!(sync_balance, 25); + assert_eq!(sync_price, 1); + assert_eq!(min_sync_interval, 10); + assert!(last_sync.is_none()); + } + ProviderRole::Primary => panic!("expected replica role"), + } + + // Only the sync_balance is reserved (payment = 0 here). + assert_eq!(Balances::free_balance(1), owner_balance_before - 25); + + // Replica provider's committed_bytes advanced. + let provider = Providers::::get(3).unwrap(); + assert_eq!(provider.committed_bytes, 50); }); } #[test] - fn request_primary_agreement_fails_for_non_admin() { + fn rejects_when_bucket_does_not_exist() { new_test_ext().execute_with(|| { - setup_provider_and_bucket(); + System::set_block_number(1); + let replica_pk = register_replica_provider(3, "//Replica"); + let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + 999, + 3, + terms, + sig, + ), + Error::::BucketNotFound + ); + }); + } + #[test] + fn rejects_when_replica_terms_missing() { + new_test_ext().execute_with(|| { + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); + + // Primary-shaped terms (no replica_params) cannot drive a replica. + let terms = primary_terms(1, 50, 100, 0, 10_000, 1); + let sig = sign_terms(&replica_pk, &terms); assert_noop!( - StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(3), // Not admin - 0, - 2, - 1000, - 100, - 1000 + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, ), - Error::::NotBucketAdmin + Error::::MissingReplicaTerms ); }); } #[test] - fn accept_agreement_works() { + fn rejects_when_agreement_already_exists() { new_test_ext().execute_with(|| { - setup_provider_and_bucket(); + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); - // max_bytes = 100 fits within stake of 200 (MinStakePerByte = 1) - // payment = price_per_byte(0) * max_bytes * duration = 0 - assert_ok!(StorageProvider::request_primary_agreement( + let terms = replica_terms(1, 10, 100, 0, 10_000, 1, 5, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_ok!(StorageProvider::establish_replica_agreement( RuntimeOrigin::signed(1), - 0, - 2, - 100, - 100, - 1000 + bucket_id, + 3, + terms, + sig, )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(2), - 0 - )); + // Same provider, same bucket → duplicate agreement. + let terms = replica_terms(1, 10, 100, 0, 10_000, 2, 5, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, + ), + Error::::AgreementAlreadyExists + ); + }); + } - // Check agreement created - let agreement = StorageAgreements::::get(0, 2).unwrap(); - assert_eq!(agreement.owner, 1); - assert_eq!(agreement.max_bytes, 100); - assert!(matches!(agreement.role, ProviderRole::Primary)); + #[test] + fn rejects_when_provider_not_accepting_replicas() { + new_test_ext().execute_with(|| { + let (bucket_id, _) = setup_primary_bucket(); + // No replica_sync_price set ⇒ not accepting replicas. + let settings = default_test_settings(0, None); + let replica_pk = register_signing_provider(3, "//Replica", 1_000, settings); - // Check provider added to bucket - let bucket = Buckets::::get(0).unwrap(); - assert!(bucket.primary_providers.contains(&2)); + let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, + ), + Error::::ProviderNotAcceptingReplicas + ); + }); + } - // Check provider stats updated - let provider = Providers::::get(2).unwrap(); - assert_eq!(provider.committed_bytes, 100); - assert_eq!(provider.stats.agreements_total, 1); + #[test] + fn rejects_owner_mismatch() { + new_test_ext().execute_with(|| { + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); + // Terms signed for owner = 1, but origin = 4. + let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(4), + bucket_id, + 3, + terms, + sig, + ), + Error::::TermsOwnerMismatch + ); + }); + } - // Check request removed - assert!(AgreementRequests::::get(0, 2).is_none()); + #[test] + fn rejects_expired_replica_terms() { + new_test_ext().execute_with(|| { + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); + + System::set_block_number(50); + let terms = replica_terms(1, 50, 100, 0, 10, 1, 25, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, + ), + Error::::TermsExpired + ); }); } #[test] - fn reject_agreement_returns_funds() { + fn rejects_invalid_replica_signature() { new_test_ext().execute_with(|| { - // Setup provider with non-zero price so funds are actually reserved - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 + let (bucket_id, _) = setup_primary_bucket(); + let _replica_pk = register_replica_provider(3, "//Replica"); + let (other_pk, _) = generate_provider_public_key("//Imposter"); + + let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let sig = sign_terms(&other_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, + ), + Error::::InvalidProviderSignature + ); + }); + } + + #[test] + fn replica_replay_window_rejects_reuse() { + new_test_ext().execute_with(|| { + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); + + let terms = replica_terms(1, 10, 100, 0, 10_000, 7, 5, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_ok!(StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms.clone(), + sig.clone(), )); - let settings = ProviderSettings { - min_duration: 0u64, - max_duration: 1000u64, - price_per_byte: 1u64, // Non-zero price - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 0, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings + // Replay same nonce → rejected (and the duplicate check would + // also trip, but nonce comes first since the same terms hash). + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, + ), + Error::::AgreementAlreadyExists + ); + }); + } +} + +mod member_buckets_tests { + use super::*; + + #[test] + fn set_member_works() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + + // Add writer + assert_ok!(StorageProvider::set_member( + RuntimeOrigin::signed(1), + 0, + 2, + Role::Writer )); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + let bucket = Buckets::::get(0).unwrap(); + assert_eq!(bucket.members.len(), 2); - let balance_before = Balances::free_balance(1); + let writer = bucket.members.iter().find(|m| m.account == 2).unwrap(); + assert_eq!(writer.role, Role::Writer); + }); + } - // Request agreement for 100 bytes at price 1, duration 10 - // payment = 1 * 100 * 10 = 1000 - assert_ok!(StorageProvider::request_primary_agreement( + #[test] + fn set_member_updates_existing_role() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + + // Add as writer + assert_ok!(StorageProvider::set_member( + RuntimeOrigin::signed(1), + 0, + 2, + Role::Writer + )); + + // Promote to admin + assert_ok!(StorageProvider::set_member( RuntimeOrigin::signed(1), 0, 2, - 100, - 10, - 1000 + Role::Admin )); - // Some funds should be reserved (1000) - assert!(Balances::free_balance(1) < balance_before); - assert_eq!(Balances::free_balance(1), balance_before - 1000); + let bucket = Buckets::::get(0).unwrap(); + let member = bucket.members.iter().find(|m| m.account == 2).unwrap(); + assert_eq!(member.role, Role::Admin); + }); + } - assert_ok!(StorageProvider::reject_agreement( - RuntimeOrigin::signed(2), - 0 + #[test] + fn set_member_fails_for_non_admin() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + + // Non-admin tries to add member + assert_noop!( + StorageProvider::set_member(RuntimeOrigin::signed(2), 0, 3, Role::Writer), + Error::::NotBucketAdmin + ); + }); + } + + #[test] + fn cannot_demote_other_admin() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + + // Add second admin + assert_ok!(StorageProvider::set_member( + RuntimeOrigin::signed(1), + 0, + 2, + Role::Admin )); - // Funds should be returned - assert_eq!(Balances::free_balance(1), balance_before); + // Admin 1 tries to demote admin 2 + assert_noop!( + StorageProvider::set_member(RuntimeOrigin::signed(1), 0, 2, Role::Writer), + Error::::CannotDemoteAdmin + ); + }); + } - // Request should be removed - assert!(AgreementRequests::::get(0, 2).is_none()); + #[test] + fn last_admin_cannot_self_demote() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + + // Admin 1 is the sole admin and cannot demote themselves. + assert_noop!( + StorageProvider::set_member(RuntimeOrigin::signed(1), 0, 1, Role::Writer), + Error::::LastAdminCannotBeRemoved + ); }); } #[test] - fn withdraw_agreement_request_works() { + fn last_admin_cannot_be_removed() { new_test_ext().execute_with(|| { - setup_provider_and_bucket(); + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); - let balance_before = Balances::free_balance(1); + assert_noop!( + StorageProvider::remove_member(RuntimeOrigin::signed(1), 0, 1), + Error::::LastAdminCannotBeRemoved + ); + }); + } + + #[test] + fn admin_can_demote_self() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + + // Add second admin + assert_ok!(StorageProvider::set_member( + RuntimeOrigin::signed(1), + 0, + 2, + Role::Admin + )); - assert_ok!(StorageProvider::request_primary_agreement( + // Admin 1 demotes self + assert_ok!(StorageProvider::set_member( + RuntimeOrigin::signed(1), + 0, + 1, + Role::Writer + )); + + let bucket = Buckets::::get(0).unwrap(); + let member = bucket.members.iter().find(|m| m.account == 1).unwrap(); + assert_eq!(member.role, Role::Writer); + }); + } + + #[test] + fn remove_member_works() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + assert_ok!(StorageProvider::set_member( RuntimeOrigin::signed(1), 0, 2, - 1000, - 100, - 1000 + Role::Writer )); - assert_ok!(StorageProvider::withdraw_agreement_request( + assert_ok!(StorageProvider::remove_member( RuntimeOrigin::signed(1), 0, 2 )); - // Funds returned - assert_eq!(Balances::free_balance(1), balance_before); + let bucket = Buckets::::get(0).unwrap(); + assert_eq!(bucket.members.len(), 1); + assert!(!bucket.members.iter().any(|m| m.account == 2)); + }); + } + + #[test] + fn remove_member_fails_for_non_existent() { + new_test_ext().execute_with(|| { + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); - // Request removed - assert!(AgreementRequests::::get(0, 2).is_none()); + assert_noop!( + StorageProvider::remove_member(RuntimeOrigin::signed(1), 0, 99), + Error::::MemberNotFound + ); }); } #[test] - fn withdraw_fails_for_non_requester() { + fn set_min_providers_works() { new_test_ext().execute_with(|| { - setup_provider_and_bucket(); + assert_ok!(StorageProvider::create_bucket_internal(&1, 2, None)); - assert_ok!(StorageProvider::request_primary_agreement( + // Can set to 0 (no minimum) + assert_ok!(StorageProvider::set_min_providers( RuntimeOrigin::signed(1), 0, - 2, - 1000, - 100, - 1000 + 0 )); - assert_noop!( - StorageProvider::withdraw_agreement_request( - RuntimeOrigin::signed(3), // Not the requester - 0, - 2 - ), - Error::::NotAgreementOwner - ); + let bucket = Buckets::::get(0).unwrap(); + assert_eq!(bucket.min_providers, 0); }); } #[test] - fn max_primary_providers_enforced() { + fn freeze_bucket_requires_snapshot() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - - // Register 6 providers (max is 5) - for i in 2..=7 { - let multiaddr = format!("/ip4/127.0.0.1/tcp/{}", 3000 + i); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(i), - multiaddr.as_bytes().to_vec().try_into().unwrap(), - test_public_key(), - 200 - )); - } - - // Add 5 providers (should all succeed) - for i in 2..=6 { - assert_ok!(StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(1), - 0, - i, - 100, - 100, - 1000 - )); - assert_ok!(StorageProvider::accept_agreement( - RuntimeOrigin::signed(i), - 0 - )); - } + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); - // 6th provider should fail assert_noop!( - StorageProvider::request_primary_agreement( - RuntimeOrigin::signed(1), - 0, - 7, - 100, - 100, - 1000 - ), - Error::::MaxPrimaryProvidersReached + StorageProvider::freeze_bucket(RuntimeOrigin::signed(1), 0), + Error::::NoSnapshot ); }); } -} - -mod member_buckets_tests { - use super::*; #[test] fn member_buckets_index_on_create() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); let member_buckets = pallet::MemberBuckets::::get(1); assert_eq!(member_buckets.to_vec(), vec![0, 1]); @@ -1452,7 +1946,7 @@ mod member_buckets_tests { #[test] fn member_buckets_index_on_set_member() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); // Add account 2 as writer assert_ok!(StorageProvider::set_member( @@ -1481,7 +1975,7 @@ mod member_buckets_tests { #[test] fn member_buckets_index_on_remove_member() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); assert_ok!(StorageProvider::set_member( RuntimeOrigin::signed(1), 0, @@ -1504,7 +1998,7 @@ mod member_buckets_tests { #[test] fn member_buckets_index_on_bucket_delete() { new_test_ext().execute_with(|| { - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 0)); + assert_ok!(StorageProvider::create_bucket_internal(&1, 0, None)); assert_ok!(StorageProvider::set_member( RuntimeOrigin::signed(1), 0, @@ -1532,9 +2026,9 @@ mod member_buckets_tests { fn member_buckets_multi_membership() { new_test_ext().execute_with(|| { // Create 3 buckets owned by different accounts - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(1), 1)); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(2), 1)); - assert_ok!(StorageProvider::create_bucket(RuntimeOrigin::signed(3), 1)); + assert_ok!(StorageProvider::create_bucket_internal(&1, 1, None)); + assert_ok!(StorageProvider::create_bucket_internal(&2, 1, None)); + assert_ok!(StorageProvider::create_bucket_internal(&3, 1, None)); // Add account 4 to all 3 buckets assert_ok!(StorageProvider::set_member( @@ -1571,302 +2065,3 @@ mod member_buckets_tests { }); } } - -mod auto_matching_tests { - use super::*; - - #[test] - fn create_bucket_with_storage_works() { - new_test_ext().execute_with(|| { - // Register a provider with accepting_primary: true - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - // Update settings to accept primary agreements - // Use price_per_byte: 0 like other tests to avoid balance issues - let settings = ProviderSettings { - min_duration: 10u64, - max_duration: 1000u64, - price_per_byte: 0u64, // Free storage (like other tests) - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 200, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - // Create bucket with storage requirements against the chosen provider - assert_ok!(StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, // explicitly selected provider - 100, // max_bytes - 100, // duration - 10 // max_price_per_byte (higher than provider's price of 0) - )); - - // Verify bucket was created - let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.min_providers, 1); - assert_eq!(bucket.primary_providers.len(), 1); - assert_eq!(bucket.primary_providers[0], 2); - - // Verify agreement was created - let agreement = StorageAgreements::::get(0, 2).unwrap(); - assert_eq!(agreement.max_bytes, 100); - assert_eq!(agreement.owner, 1); - - // Verify provider's committed_bytes was updated - let provider = Providers::::get(2).unwrap(); - assert_eq!(provider.committed_bytes, 100); - }); - } - - #[test] - fn create_bucket_with_storage_fails_provider_not_found() { - new_test_ext().execute_with(|| { - // Provider 2 is not registered - assert_noop!( - StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, - 100, - 100, - 10 - ), - Error::::ProviderNotFound - ); - }); - } - - #[test] - fn create_bucket_with_storage_fails_provider_not_accepting() { - new_test_ext().execute_with(|| { - // Register a provider but don't set accepting_primary: true - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - // Settings have accepting_primary: false by default (need to explicitly enable) - // Since default is accepting_primary: true, let's set it to false - let settings = ProviderSettings { - min_duration: 10u64, - max_duration: 1000u64, - price_per_byte: 1u64, - accepting_primary: false, // Not accepting - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 200, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - assert_noop!( - StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, - 100, - 100, - 10 - ), - Error::::ProviderNotAcceptingPrimary - ); - }); - } - - #[test] - fn create_bucket_with_storage_fails_price_too_high() { - new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - // Provider with high price - let settings = ProviderSettings { - min_duration: 10u64, - max_duration: 1000u64, - price_per_byte: 100u64, // Very high price - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 200, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - // User's max_price_per_byte is lower than provider's price - assert_noop!( - StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, - 100, - 100, - 10 // max_price_per_byte is 10, but provider charges 100 - ), - Error::::PriceExceedsMax - ); - }); - } - - #[test] - fn create_bucket_with_storage_fails_insufficient_capacity() { - new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - let settings = ProviderSettings { - min_duration: 10u64, - max_duration: 1000u64, - price_per_byte: 1u64, - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 50, // Only 50 bytes capacity - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - // Request 100 bytes, but provider only has 50 - assert_noop!( - StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, - 100, // Needs 100 bytes - 100, - 10 - ), - Error::::CapacityExceeded - ); - }); - } - - #[test] - fn create_bucket_with_storage_fails_duration_mismatch() { - new_test_ext().execute_with(|| { - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - - let settings = ProviderSettings { - min_duration: 500u64, // Minimum 500 blocks - max_duration: 1000u64, - price_per_byte: 1u64, - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 200, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings - )); - - // Request only 100 blocks, but provider requires minimum 500 - assert_noop!( - StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, - 100, - 100, // Duration of 100, below provider's min of 500 - 10 - ), - Error::::DurationTooShort - ); - }); - } - - #[test] - fn create_bucket_with_storage_honors_explicit_provider() { - new_test_ext().execute_with(|| { - // Register two eligible providers; the caller picks which one to use. - let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - - // Provider 2: price = 5 - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(2), - multiaddr.clone().try_into().unwrap(), - test_public_key(), - 200 - )); - let settings_expensive = ProviderSettings { - min_duration: 10u64, - max_duration: 1000u64, - price_per_byte: 5u64, - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 200, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(2), - settings_expensive - )); - - // Provider 3: price = 0 - assert_ok!(StorageProvider::register_provider( - RuntimeOrigin::signed(3), - multiaddr.try_into().unwrap(), - test_public_key(), - 200 - )); - let settings_cheap = ProviderSettings { - min_duration: 10u64, - max_duration: 1000u64, - price_per_byte: 0u64, // Free - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 200, - }; - assert_ok!(StorageProvider::update_provider_settings( - RuntimeOrigin::signed(3), - settings_cheap - )); - - // Caller explicitly selects provider 2, even though 3 is cheaper. - // Use small values to keep payment low: 10 * 10 * 5 = 500 max - assert_ok!(StorageProvider::create_bucket_with_storage( - RuntimeOrigin::signed(1), - 2, // explicitly selected provider - 10, // max_bytes - 10, // duration - 10 // max_price_per_byte - )); - - // Verify the bucket opened the agreement with the chosen provider (2). - let bucket = Buckets::::get(0).unwrap(); - assert_eq!(bucket.primary_providers[0], 2); - assert!(StorageAgreements::::contains_key(0, 2)); - }); - } -} From 583e1af6a4b8ae81f0b193fc23c930dd88d811a5 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 18:00:14 +0700 Subject: [PATCH 06/44] feat: update pallet storage provider benchmarks - Remove benchmarks for deleted extrinsics. - Add establish_storage_agreement and establish_replica_agreement benchmarks covering signature verification + replay-window mutation + bucket / agreement insertion costs. - Update helper functions, other benchmarks regarding new changes. --- pallet/src/benchmarking.rs | 461 +++++++++++++++++-------------------- 1 file changed, 207 insertions(+), 254 deletions(-) diff --git a/pallet/src/benchmarking.rs b/pallet/src/benchmarking.rs index f715e524..2e210596 100644 --- a/pallet/src/benchmarking.rs +++ b/pallet/src/benchmarking.rs @@ -8,10 +8,13 @@ use frame_system::{pallet_prelude::BlockNumberFor, Pallet as System, RawOrigin}; use sp_core::H256; use sp_runtime::traits::{Bounded, SaturatedConversion}; use sp_runtime::Saturating; -use storage_primitives::{BucketId, ReplicaRequestParams}; +use storage_primitives::{AgreementTerms, BucketId, ProviderRole, ReplicaTerms}; const SEED: u32 = 0; +/// Key type used by the benchmarking keystore for provider signing material. +const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_core::crypto::KeyTypeId(*b"bnch"); + fn funded_account(name: &'static str, index: u32) -> T::AccountId { let account: T::AccountId = account(name, index, SEED); let amount = BalanceOf::::max_value() / 2u32.into(); @@ -54,31 +57,145 @@ fn create_provider(index: u32) -> T::AccountId { fn setup_bucket(admin: &T::AccountId) -> BucketId { // Use min_providers=0 so benchmarks can create checkpoints with empty signatures - let _ = Pallet::::create_bucket(RawOrigin::Signed(admin.clone()).into(), 0); - NextBucketId::::get() - 1 + Pallet::::create_bucket_internal(admin, 0, None).expect("create_bucket_internal succeeds") +} + +/// Build primary [`AgreementTerms`] suitable for a benchmark agreement. +fn build_primary_terms( + owner: &T::AccountId, + max_bytes: u64, + duration: BlockNumberFor, + nonce: u64, +) -> AgreementTermsOf { + AgreementTerms { + owner: owner.clone(), + max_bytes, + duration, + price_per_byte: 1u32.into(), + valid_until: BlockNumberFor::::max_value(), + nonce, + replica_params: None, + } +} + +/// Build replica [`AgreementTerms`] suitable for a benchmark agreement. +fn build_replica_terms( + owner: &T::AccountId, + max_bytes: u64, + duration: BlockNumberFor, + nonce: u64, +) -> AgreementTermsOf { + AgreementTerms { + owner: owner.clone(), + max_bytes, + duration, + price_per_byte: 1u32.into(), + valid_until: BlockNumberFor::::max_value(), + nonce, + replica_params: Some(ReplicaTerms { + sync_balance: BalanceOf::::max_value() / 20u32.into(), + min_sync_interval: 10u32.into(), + }), + } +} + +/// Sign SCALE-encoded terms with the provider's sr25519 keystore key. +fn sign_terms( + public_key: &sp_core::sr25519::Public, + terms: &AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&codec::Encode::encode(terms)); + let sig = sp_io::crypto::sr25519_sign(KEY_TYPE, public_key, &hash) + .expect("benchmarking keystore signs with a key it generated"); + sp_runtime::MultiSignature::Sr25519(sig) } +/// Open a primary agreement for the provider, atomically creating the bucket. +/// +/// Replaces the legacy `request_primary_agreement` + `accept_agreement` pair: +/// generates an sr25519 key for the provider, signs primary terms, and calls +/// `establish_storage_agreement_internal`. Returns the new bucket id. fn setup_primary_agreement( + admin: &T::AccountId, + provider: &T::AccountId, + provider_index: u32, +) -> BucketId { + let key = register_sr25519_key::(provider, KEY_TYPE, provider_index); + let terms = build_primary_terms::( + admin, + 1_000_000u64, + 100u32.into(), + provider_index as u64 + 1, + ); + let sig = sign_terms::(&key, &terms); + Pallet::::establish_storage_agreement_internal(admin, provider, terms, &sig) + .expect("establish_storage_agreement_internal succeeds") +} + +/// Open a replica agreement against an existing bucket. +/// +/// Generates an sr25519 key for the replica provider, signs replica terms, +/// and calls `establish_replica_agreement_internal`. +fn setup_replica_agreement( + admin: &T::AccountId, + bucket_id: BucketId, + replica: &T::AccountId, + replica_index: u32, +) { + let key = register_sr25519_key::(replica, KEY_TYPE, replica_index); + let terms = build_replica_terms::( + admin, + 1_000_000u64, + 100u32.into(), + replica_index as u64 + 1, + ); + let sig = sign_terms::(&key, &terms); + Pallet::::establish_replica_agreement_internal(admin, bucket_id, replica, terms, &sig) + .expect("establish_replica_agreement_internal succeeds"); +} + +/// Direct-storage helper: register `provider` as a primary on an existing +/// bucket without going through `establish_storage_agreement_internal`. +/// +/// `establish_storage_agreement_internal` always creates a fresh +/// single-primary bucket, so it can't grow the primary set on an existing +/// bucket. The checkpoint benchmarks need *N* primaries on the *same* +/// bucket to exercise worst-case signature verification, so we synthesize +/// that shape directly. +fn add_primary_to_bucket( admin: &T::AccountId, provider: &T::AccountId, bucket_id: BucketId, + max_bytes: u64, ) { - let max_bytes = 1_000_000u64; + let current_block = System::::block_number(); let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); + let expires_at = current_block.saturating_add(duration); - // Request primary agreement - let _ = Pallet::::request_primary_agreement( - RawOrigin::Signed(admin.clone()).into(), - bucket_id, - provider.clone(), + Buckets::::mutate(bucket_id, |maybe| { + if let Some(b) = maybe { + let _ = b.primary_providers.try_push(provider.clone()); + } + }); + + let agreement = StorageAgreement:: { + owner: admin.clone(), max_bytes, - duration, - payment, - ); + payment_locked: 0u32.into(), + price_per_byte: 1u32.into(), + expires_at, + extensions_blocked: false, + role: ProviderRole::Primary, + started_at: current_block, + }; + StorageAgreements::::insert(bucket_id, provider, agreement); - // Accept agreement - let _ = Pallet::::accept_agreement(RawOrigin::Signed(provider.clone()).into(), bucket_id); + Providers::::mutate(provider, |maybe| { + if let Some(p) = maybe { + p.committed_bytes = p.committed_bytes.saturating_add(max_bytes); + p.stats.agreements_total = p.stats.agreements_total.saturating_add(1); + } + }); } /// Insert a single challenge at `deadline = 200` and return its `ChallengeId`. @@ -130,9 +247,6 @@ mod benchmarks { use super::*; use frame_system::pallet_prelude::BlockNumberFor; - // Key type used for sr25519 key generation in benchmarks - const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_core::crypto::KeyTypeId(*b"bnch"); - // ───────────────────────────────────────────────────────────────────────── // Provider Management // ───────────────────────────────────────────────────────────────────────── @@ -247,8 +361,7 @@ mod benchmarks { fn block_extensions() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); #[extrinsic_call] set_extensions_blocked(RawOrigin::Signed(provider), bucket_id, true); @@ -270,34 +383,6 @@ mod benchmarks { // Bucket Management // ───────────────────────────────────────────────────────────────────────── - #[benchmark] - fn create_bucket() { - let admin = funded_account::("admin", 0); - - #[extrinsic_call] - create_bucket(RawOrigin::Signed(admin), 1); - } - - #[benchmark] - fn create_bucket_with_storage() { - // Create a provider first - let provider = create_provider::(0); - let admin = funded_account::("admin", 1); - - let max_bytes = 1_000u64; - let duration: BlockNumberFor = 100u32.into(); - let max_price_per_byte: BalanceOf = 1000u32.into(); - - #[extrinsic_call] - create_bucket_with_storage( - RawOrigin::Signed(admin), - provider, - max_bytes, - duration, - max_price_per_byte, - ); - } - #[benchmark] fn set_bucket_min_providers() { let admin = funded_account::("admin", 0); @@ -311,8 +396,12 @@ mod benchmarks { fn freeze_bucket() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); + + // `freeze_bucket` requires the `snapshot` to meet `min_providers`. + // Drop the floor to 0 first so both subsequent calls succeed. + let _ = + Pallet::::set_min_providers(RawOrigin::Signed(admin.clone()).into(), bucket_id, 0); // Need to create a checkpoint first let mmr_root = H256::repeat_byte(0xAB); @@ -330,10 +419,6 @@ mod benchmarks { signatures, ); - // Set min_providers to 0 so freeze succeeds - let _ = - Pallet::::set_min_providers(RawOrigin::Signed(admin.clone()).into(), bucket_id, 0); - #[extrinsic_call] freeze_bucket(RawOrigin::Signed(admin), bucket_id); } @@ -375,8 +460,7 @@ mod benchmarks { fn remove_slashed() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // Simulate slashing: set provider stake to zero Providers::::mutate(&provider, |maybe_provider| { @@ -393,127 +477,51 @@ mod benchmarks { // Agreement Management // ───────────────────────────────────────────────────────────────────────── + /// Worst case: full signature verification + replay-window mutation + + /// bucket creation + agreement insertion. #[benchmark] - fn request_agreement() { + fn establish_storage_agreement() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); - // Create a second provider for replica agreement - let replica_provider = create_provider::(1); - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - let replica_params = ReplicaRequestParams { - sync_balance: BalanceOf::::max_value() / 20u32.into(), - min_sync_interval: 10u32.into(), - }; + // Generate an sr25519 key for the provider and store it so + // verify_terms_signature can resolve a valid signer. + let key = register_sr25519_key::(&provider, KEY_TYPE, 0); + let terms = build_primary_terms::(&admin, 1_000_000u64, 100u32.into(), 1); + let signature = sign_terms::(&key, &terms); #[extrinsic_call] - request_agreement( - RawOrigin::Signed(admin), - bucket_id, - replica_provider, - max_bytes, - duration, - payment, - replica_params, - ); + establish_storage_agreement(RawOrigin::Signed(admin), provider, terms, signature); } + /// Worst case: replica signature verification + replay-window + /// mutation + agreement insertion on top of an existing bucket. #[benchmark] - fn request_primary_agreement() { + fn establish_replica_agreement() { let admin = funded_account::("admin", 0); - let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); + let primary = create_provider::(0); + let bucket_id = setup_primary_agreement::(&admin, &primary, 0); - #[extrinsic_call] - request_primary_agreement( - RawOrigin::Signed(admin), - bucket_id, - provider, - max_bytes, - duration, - payment, - ); - } - - #[benchmark] - fn accept_agreement() { - let admin = funded_account::("admin", 0); - let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - - let _ = Pallet::::request_primary_agreement( - RawOrigin::Signed(admin).into(), - bucket_id, - provider.clone(), - max_bytes, - duration, - payment, - ); - - #[extrinsic_call] - accept_agreement(RawOrigin::Signed(provider), bucket_id); - } - - #[benchmark] - fn reject_agreement() { - let admin = funded_account::("admin", 0); - let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - - let _ = Pallet::::request_primary_agreement( - RawOrigin::Signed(admin).into(), - bucket_id, - provider.clone(), - max_bytes, - duration, - payment, - ); + let replica = create_provider::(1); + let key = register_sr25519_key::(&replica, KEY_TYPE, 1); + let terms = build_replica_terms::(&admin, 1_000_000u64, 100u32.into(), 1); + let signature = sign_terms::(&key, &terms); #[extrinsic_call] - reject_agreement(RawOrigin::Signed(provider), bucket_id); - } - - #[benchmark] - fn withdraw_agreement_request() { - let admin = funded_account::("admin", 0); - let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - - let _ = Pallet::::request_primary_agreement( - RawOrigin::Signed(admin.clone()).into(), + establish_replica_agreement( + RawOrigin::Signed(admin), bucket_id, - provider.clone(), - max_bytes, - duration, - payment, + replica, + terms, + signature, ); - - #[extrinsic_call] - withdraw_agreement_request(RawOrigin::Signed(admin), bucket_id, provider); } #[benchmark] fn top_up_agreement() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); let additional_bytes = 500_000u64; let max_payment = BalanceOf::::max_value() / 10u32.into(); @@ -532,8 +540,7 @@ mod benchmarks { fn extend_agreement() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); let additional_duration: BlockNumberFor = 50u32.into(); let max_payment = BalanceOf::::max_value() / 10u32.into(); @@ -552,8 +559,7 @@ mod benchmarks { fn end_agreement(a: Linear<0, 1>) { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // a == 0 → Pay (single transfer to provider) // a == 1 → Burn with burn_percent in (0, 100) → both owner→provider @@ -579,8 +585,7 @@ mod benchmarks { fn claim_expired_agreement() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // Advance block past agreement expiry + settlement timeout let agreement = StorageProvider::::storage_agreements(bucket_id, &provider).unwrap(); @@ -614,7 +619,7 @@ mod benchmarks { for i in 0..n { let provider = create_provider::(i); let key = register_sr25519_key::(&provider, KEY_TYPE, i); - setup_primary_agreement::(&admin, &provider, bucket_id); + add_primary_to_bucket::(&admin, &provider, bucket_id, 1_000_000); provider_keys.push((provider, key)); } @@ -660,7 +665,7 @@ mod benchmarks { for i in 0..n { let provider = create_provider::(i); let key = register_sr25519_key::(&provider, KEY_TYPE, i); - setup_primary_agreement::(&admin, &provider, bucket_id); + add_primary_to_bucket::(&admin, &provider, bucket_id, 1_000_000); provider_keys.push((provider, key)); } @@ -730,7 +735,7 @@ mod benchmarks { for i in 0..s { let provider = create_provider::(i); let key = register_sr25519_key::(&provider, KEY_TYPE, i); - setup_primary_agreement::(&admin, &provider, bucket_id); + add_primary_to_bucket::(&admin, &provider, bucket_id, 1_000_000); provider_keys.push((provider, key)); } @@ -802,8 +807,7 @@ mod benchmarks { fn report_missed_checkpoint() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // Report window 1 — must satisfy current_block > window_start_block(window+1, interval). // Target: interval * (window + 1) + 1 @@ -821,8 +825,7 @@ mod benchmarks { fn claim_checkpoint_rewards() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // Directly write rewards to storage let reward: BalanceOf = 1000u32.into(); @@ -840,8 +843,13 @@ mod benchmarks { fn challenge_checkpoint() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); + + // Drop min_providers to 0 so the bootstrapping checkpoint with + // empty signatures is accepted (establish_storage_agreement_internal + // anchors the bucket at min_providers=1). + let _ = + Pallet::::set_min_providers(RawOrigin::Signed(admin.clone()).into(), bucket_id, 0); // Create checkpoint first let mmr_root = H256::repeat_byte(0xAB); @@ -879,8 +887,7 @@ mod benchmarks { fn challenge_off_chain() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // Generate sr25519 keypair via host functions (works in no_std benchmarks) let key_type = sp_core::crypto::KeyTypeId(*b"bnch"); @@ -917,8 +924,12 @@ mod benchmarks { let admin = funded_account::("admin", 0); let provider = create_provider::(0); let replica_provider = create_provider::(1); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); + + // Drop min_providers to 0 so the bootstrapping checkpoint with + // empty signatures is accepted. + let _ = + Pallet::::set_min_providers(RawOrigin::Signed(admin.clone()).into(), bucket_id, 0); // Create checkpoint so bucket has a snapshot let mmr_root = H256::repeat_byte(0xAB); @@ -935,29 +946,8 @@ mod benchmarks { signatures, ); - // Create replica agreement - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - let replica_params = ReplicaRequestParams { - sync_balance: BalanceOf::::max_value() / 20u32.into(), - min_sync_interval: 10u32.into(), - }; - - let _ = Pallet::::request_agreement( - RawOrigin::Signed(admin.clone()).into(), - bucket_id, - replica_provider.clone(), - max_bytes, - duration, - payment, - replica_params, - ); - - let _ = Pallet::::accept_agreement( - RawOrigin::Signed(replica_provider.clone()).into(), - bucket_id, - ); + // Open the replica agreement via the signed-terms helper. + setup_replica_agreement::(&admin, bucket_id, &replica_provider, 1); // Confirm replica sync so replica has a last_sync root let roots: [Option; 7] = [Some(mmr_root), None, None, None, None, None, None]; @@ -979,8 +969,7 @@ mod benchmarks { fn respond_to_challenge_proof() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // Construct a single-chunk / single-leaf MMR so all proof verifications pass. // @@ -1036,8 +1025,7 @@ mod benchmarks { fn respond_to_challenge_deleted() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); // `verify_signature` looks up the signer (admin) in `Providers`, so admin // must have a provider record with a valid sr25519 public key. @@ -1077,8 +1065,12 @@ mod benchmarks { fn respond_to_challenge_superseded() { let admin = funded_account::("admin", 0); let provider = create_provider::(0); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); + + // Drop min_providers to 0 so the bootstrapping checkpoint below + // with empty signatures is accepted. + let _ = + Pallet::::set_min_providers(RawOrigin::Signed(admin.clone()).into(), bucket_id, 0); // `Superseded` requires the bucket to have a snapshot whose // (start_seq + leaf_count) exceeds the challenged sequence. A @@ -1116,8 +1108,12 @@ mod benchmarks { let admin = funded_account::("admin", 0); let provider = create_provider::(0); let replica_provider = create_provider::(1); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); + + // Drop min_providers to 0 so the bootstrapping checkpoint with + // empty signatures is accepted. + let _ = + Pallet::::set_min_providers(RawOrigin::Signed(admin.clone()).into(), bucket_id, 0); // Create checkpoint so bucket has a snapshot with known mmr_root let mmr_root = H256::repeat_byte(0xAB); @@ -1134,29 +1130,8 @@ mod benchmarks { signatures, ); - // Create replica agreement - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - let replica_params = ReplicaRequestParams { - sync_balance: BalanceOf::::max_value() / 20u32.into(), - min_sync_interval: 10u32.into(), - }; - - let _ = Pallet::::request_agreement( - RawOrigin::Signed(admin).into(), - bucket_id, - replica_provider.clone(), - max_bytes, - duration, - payment, - replica_params, - ); - - let _ = Pallet::::accept_agreement( - RawOrigin::Signed(replica_provider.clone()).into(), - bucket_id, - ); + // Open the replica agreement via the signed-terms helper. + setup_replica_agreement::(&admin, bucket_id, &replica_provider, 1); // roots[0] matches current snapshot mmr_root let roots: [Option; 7] = [Some(mmr_root), None, None, None, None, None, None]; @@ -1177,32 +1152,10 @@ mod benchmarks { let admin = funded_account::("admin", 0); let provider = create_provider::(0); let replica_provider = create_provider::(1); - let bucket_id = setup_bucket::(&admin); - setup_primary_agreement::(&admin, &provider, bucket_id); + let bucket_id = setup_primary_agreement::(&admin, &provider, 0); - // Create replica agreement - let max_bytes = 1_000_000u64; - let duration: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - let replica_params = ReplicaRequestParams { - sync_balance: BalanceOf::::max_value() / 20u32.into(), - min_sync_interval: 10u32.into(), - }; - - let _ = Pallet::::request_agreement( - RawOrigin::Signed(admin.clone()).into(), - bucket_id, - replica_provider.clone(), - max_bytes, - duration, - payment, - replica_params, - ); - - let _ = Pallet::::accept_agreement( - RawOrigin::Signed(replica_provider.clone()).into(), - bucket_id, - ); + // Open the replica agreement via the signed-terms helper. + setup_replica_agreement::(&admin, bucket_id, &replica_provider, 1); let top_up_amount = BalanceOf::::max_value() / 50u32.into(); From 2e10aaccb2d87093419b75354d51a7e332343dc8 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 18:02:02 +0700 Subject: [PATCH 07/44] feat: update pallet s3 registry - Update create_s3_bucket now takes (name, provider, terms, sig) and calls establish_storage_agreement_internal for the Layer 0 bucket + primary agreement atomically. - `create_s3_bucket_with_storage` is removed. - Drop NoProvidersAvailable, AgreementRequestFailed, Layer0BucketCreationFailed errors, and return Layer 0 errors directly. - Update tests following changes --- Cargo.lock | 2 + .../s3/pallet-s3-registry/Cargo.toml | 7 +- .../s3/pallet-s3-registry/src/benchmarking.rs | 97 +++--- .../s3/pallet-s3-registry/src/lib.rs | 120 +------ .../s3/pallet-s3-registry/src/mock.rs | 3 + .../s3/pallet-s3-registry/src/tests.rs | 317 +++++++++--------- 6 files changed, 225 insertions(+), 321 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13e2d7a1..fd70603a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4664,7 +4664,9 @@ dependencies = [ "scale-info", "sp-core", "sp-io", + "sp-keystore", "sp-runtime", + "storage-primitives", ] [[package]] diff --git a/storage-interfaces/s3/pallet-s3-registry/Cargo.toml b/storage-interfaces/s3/pallet-s3-registry/Cargo.toml index 757f8f31..a1fc0e60 100644 --- a/storage-interfaces/s3/pallet-s3-registry/Cargo.toml +++ b/storage-interfaces/s3/pallet-s3-registry/Cargo.toml @@ -11,6 +11,7 @@ description = "S3 bucket and object registry pallet for web3-storage" # Internal s3-primitives = { workspace = true } pallet-storage-provider = { workspace = true } +storage-primitives = { workspace = true } # Parity codec codec = { workspace = true } @@ -21,12 +22,14 @@ frame-support = { workspace = true } frame-system = { workspace = true } frame-benchmarking = { workspace = true, optional = true } sp-core = { workspace = true } +sp-io = { workspace = true, optional = true } sp-runtime = { workspace = true } [dev-dependencies] pallet-balances = { workspace = true, features = ["std"] } pallet-timestamp = { workspace = true, features = ["std"] } sp-io = { workspace = true, features = ["std"] } +sp-keystore = { workspace = true, features = ["std"] } [features] default = ["std"] @@ -41,10 +44,12 @@ std = [ "s3-primitives/std", "scale-info/std", "sp-core/std", - "sp-io/std", + "sp-io?/std", "sp-runtime/std", + "storage-primitives/std", ] runtime-benchmarks = [ + "dep:sp-io", "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", diff --git a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs index bb775c3f..3c7c7162 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs @@ -19,9 +19,6 @@ //! * `copy_object_metadata` — distinct src and dst buckets, both keys at //! max length, source object carries max-size metadata, dst hits the //! new-object branch with `object_count` at `MaxObjectsPerBucket - 1`. -//! * `create_s3_bucket_with_storage` — `MaxPrimaryProviders` providers -//! registered so `query_available_providers` iterates the full set; name -//! at max length; `UserBuckets` pre-filled to the capacity boundary. use super::*; use alloc::{vec, vec::Vec}; @@ -30,8 +27,10 @@ use frame_support::{ traits::{Currency, Get}, BoundedVec, }; -use frame_system::RawOrigin; -use pallet_storage_provider::{Pallet as StorageProvider, ProviderSettings}; +use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; +use pallet_storage_provider::{ + AgreementTermsOf, Pallet as StorageProvider, ProviderSettings, +}; use s3_primitives::{ BucketName, MaxContentTypeLen, MaxEtagLen, MaxMetadataEntries, MaxMetadataKeyLen, MaxMetadataValueLen, MaxObjectKeyLen, MetadataEntry, ObjectKey, ObjectMetadata, S3BucketId, @@ -39,9 +38,13 @@ use s3_primitives::{ }; use sp_core::H256; use sp_runtime::traits::{Bounded, SaturatedConversion}; +use storage_primitives::AgreementTerms; const SEED: u32 = 0; +/// Key type used by the benchmarking keystore for provider signing material. +const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_core::crypto::KeyTypeId(*b"s3bn"); + /// Account with effectively unbounded balance. fn funded_account(name: &'static str, index: u32) -> T::AccountId { let acc: T::AccountId = account(name, index, SEED); @@ -51,11 +54,17 @@ fn funded_account(name: &'static str, index: u32) -> T::AccountId { } /// Register a storage provider that accepts primary agreements with enough -/// stake/capacity to back the benchmarks that open agreements. -fn create_provider(index: u32) -> T::AccountId { +/// stake/capacity to back the benchmarks that open agreements. Returns both +/// the account and the sr25519 public key registered against it so the +/// `create_s3_bucket` benchmark can sign terms with the matching keystore key. +fn create_provider(index: u32) -> (T::AccountId, sp_core::sr25519::Public) { let provider = funded_account::("provider", index); let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - let public_key = [0u8; 32].to_vec(); + + // Generate an sr25519 key in the runtime keystore so the pallet can + // verify signatures over agreement terms. + let seed = alloc::format!("//S3BenchProvider{index}"); + let public_key = sp_io::crypto::sr25519_generate(KEY_TYPE, Some(seed.into_bytes())); let capacity: u64 = 1_000_000_000; let capacity_balance: BalanceOf = capacity.saturated_into(); @@ -65,7 +74,7 @@ fn create_provider(index: u32) -> T::AccountId { let _ = StorageProvider::::register_provider( RawOrigin::Signed(provider.clone()).into(), multiaddr.try_into().unwrap(), - public_key.try_into().unwrap(), + public_key.0.to_vec().try_into().unwrap(), stake, ); @@ -82,7 +91,18 @@ fn create_provider(index: u32) -> T::AccountId { }, ); - provider + (provider, public_key) +} + +/// Sign agreement terms with the provider's keystore key. +fn sign_terms( + public_key: &sp_core::sr25519::Public, + terms: &AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&codec::Encode::encode(terms)); + let sig = sp_io::crypto::sr25519_sign(KEY_TYPE, public_key, &hash) + .expect("benchmarking keystore signs with a key it generated"); + sp_runtime::MultiSignature::Sr25519(sig) } /// Generate a valid S3 bucket name of `len` bytes (3 ≤ len ≤ 63), made of @@ -186,11 +206,16 @@ mod benchmarks { // ───────────────────────────────────────────────────────────────────────── /// Worst case: + /// - Provider registered with full max-capacity stake so the Layer 0 + /// signature / replay / capacity / stake / duration / price checks + /// all run. /// - Name is at the max 63 bytes that `validate_bucket_name` accepts. /// - `UserBuckets` for the caller is pre-filled to `MaxBucketsPerUser - 1` /// so the bounded `try_push` runs right at the capacity boundary. #[benchmark] fn create_s3_bucket() { + let (provider, provider_pk) = create_provider::(0); + let user = funded_account::("user", 0); let cap = T::MaxBucketsPerUser::get(); if cap > 1 { @@ -198,9 +223,21 @@ mod benchmarks { } let name = make_bucket_name(63, 0); + let max_bytes: u64 = 1_000; + let duration: BlockNumberFor = 100u32.into(); + let terms: AgreementTermsOf = AgreementTerms { + owner: user.clone(), + max_bytes, + duration, + price_per_byte: 1u32.into(), + valid_until: BlockNumberFor::::max_value(), + nonce: 1, + replica_params: None, + }; + let sig = sign_terms::(&provider_pk, &terms); #[extrinsic_call] - create_s3_bucket(RawOrigin::Signed(user), name, 1); + create_s3_bucket(RawOrigin::Signed(user), name, provider, terms, sig); } /// Worst case: @@ -335,43 +372,5 @@ mod benchmarks { ); } - // ───────────────────────────────────────────────────────────────────────── - // Atomic create-with-storage - // ───────────────────────────────────────────────────────────────────────── - - /// Worst case: - /// - `MaxPrimaryProviders` providers registered so - /// `query_available_providers` iterates the full set. - /// - Bucket name at max length. - /// - `UserBuckets` pre-filled to `MaxBucketsPerUser - 1` so the - /// bounded `try_push` happens at the capacity boundary. - #[benchmark] - fn create_s3_bucket_with_storage() { - let n = ::MaxPrimaryProviders::get(); - for i in 0..n { - let _ = create_provider::(i); - } - - let user = funded_account::("user", 0); - let cap = T::MaxBucketsPerUser::get(); - if cap > 1 { - prefill_user_buckets::(&user, cap - 1); - } - - let name = make_bucket_name(63, 6); - let max_capacity: u64 = 1_000; - let duration: BlockNumberFor = 100u32.into(); - let max_payment = BalanceOf::::max_value() / 10u32.into(); - - #[extrinsic_call] - create_s3_bucket_with_storage( - RawOrigin::Signed(user), - name, - max_capacity, - duration, - max_payment, - ); - } - impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/storage-interfaces/s3/pallet-s3-registry/src/lib.rs b/storage-interfaces/s3/pallet-s3-registry/src/lib.rs index 5a2e72cc..f5e9ba18 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/lib.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/lib.rs @@ -161,12 +161,6 @@ pub mod pallet { ObjectKeyTooLong, /// Content type too long. ContentTypeTooLong, - /// Layer 0 bucket creation failed. - Layer0BucketCreationFailed, - /// No storage providers available for the requested capacity. - NoProvidersAvailable, - /// Failed to request storage agreement with provider. - AgreementRequestFailed, } #[pallet::call] @@ -178,13 +172,17 @@ pub mod pallet { /// /// Parameters: /// - `name`: S3 bucket name (3-63 chars, lowercase alphanumeric + hyphens) - /// - `min_providers`: Minimum number of storage providers required + /// - `provider`: Explicit provider account that signed the terms. + /// - `terms`: Provider-signed agreement terms. + /// - `sig`: Provider signature over the SCALE-encoded terms. #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::create_s3_bucket())] pub fn create_s3_bucket( origin: OriginFor, name: Vec, - min_providers: u32, + provider: T::AccountId, + terms: pallet_storage_provider::AgreementTermsOf, + sig: sp_runtime::MultiSignature, ) -> DispatchResult { let who = ensure_signed(origin)?; @@ -210,9 +208,13 @@ pub mod pallet { Error::::TooManyBuckets ); - // Create Layer 0 bucket internally (this makes caller the admin) + // Create Layer 0 bucket + primary agreement atomically. Layer 0 + // errors (bad signature, replay, capacity, price, …) surface + // directly so callers can act on them. let layer0_bucket_id = - pallet_storage_provider::Pallet::::create_bucket_internal(&who, min_providers)?; + pallet_storage_provider::Pallet::::establish_storage_agreement_internal( + &who, &provider, terms, &sig, + )?; // Generate new S3 bucket ID let s3_bucket_id = NextS3BucketId::::get(); @@ -481,104 +483,6 @@ pub mod pallet { Ok(()) } - /// Create an S3 bucket with automatic provider discovery and agreement request. - /// - /// This atomically creates a Layer 0 bucket, finds an available provider, - /// and requests a primary storage agreement — all in a single transaction. - /// - /// Parameters: - /// - `name`: S3 bucket name (3-63 chars, lowercase alphanumeric + hyphens) - /// - `max_capacity`: Maximum storage capacity in bytes - /// - `duration`: Storage duration in blocks - /// - `max_payment`: Maximum payment for the storage agreement - #[pallet::call_index(5)] - #[pallet::weight(::WeightInfo::create_s3_bucket_with_storage())] - pub fn create_s3_bucket_with_storage( - origin: OriginFor, - name: Vec, - max_capacity: u64, - duration: BlockNumberFor, - max_payment: BalanceOf, - ) -> DispatchResult { - let who = ensure_signed(origin)?; - - // Validate bucket name - ensure!(validate_bucket_name(&name), Error::::InvalidBucketName); - - let bounded_name: BucketName = name - .clone() - .try_into() - .map_err(|_| Error::::InvalidBucketName)?; - - // Check name uniqueness - ensure!( - !BucketNameToId::::contains_key(&bounded_name), - Error::::BucketNameExists - ); - - // Check user bucket limit - let mut user_buckets = UserBuckets::::get(&who); - ensure!( - user_buckets.len() < T::MaxBucketsPerUser::get() as usize, - Error::::TooManyBuckets - ); - - // Step 1: Create Layer 0 bucket - let layer0_bucket_id = - pallet_storage_provider::Pallet::::create_bucket_internal(&who, 1) - .map_err(|_| Error::::Layer0BucketCreationFailed)?; - - // Step 2: Find an available provider - let available_providers = - pallet_storage_provider::Pallet::::query_available_providers(max_capacity, true); - ensure!( - !available_providers.is_empty(), - Error::::NoProvidersAvailable - ); - let provider = &available_providers[0]; - - // Step 3: Request primary agreement - pallet_storage_provider::Pallet::::request_primary_agreement_internal( - &who, - layer0_bucket_id, - provider, - max_capacity, - duration, - max_payment, - ) - .map_err(|_| Error::::AgreementRequestFailed)?; - - // Step 4: Create S3 metadata - let s3_bucket_id = NextS3BucketId::::get(); - NextS3BucketId::::put(s3_bucket_id.saturating_add(1)); - - let bucket_info = S3BucketInfo { - s3_bucket_id, - name: bounded_name.clone(), - layer0_bucket_id, - owner: who.clone(), - created_at: frame_system::Pallet::::block_number(), - object_count: 0, - total_size: 0, - }; - - S3Buckets::::insert(s3_bucket_id, bucket_info); - BucketNameToId::::insert(&bounded_name, s3_bucket_id); - - user_buckets - .try_push(s3_bucket_id) - .map_err(|_| Error::::TooManyBuckets)?; - UserBuckets::::insert(&who, user_buckets); - - Self::deposit_event(Event::S3BucketCreated { - s3_bucket_id, - name, - layer0_bucket_id, - owner: who, - }); - - Ok(()) - } } impl Pallet { diff --git a/storage-interfaces/s3/pallet-s3-registry/src/mock.rs b/storage-interfaces/s3/pallet-s3-registry/src/mock.rs index a34a0cb3..b20dc878 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/mock.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/mock.rs @@ -133,6 +133,9 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .unwrap(); let mut ext = sp_io::TestExternalities::new(t); + ext.register_extension(sp_keystore::KeystoreExt::new( + sp_keystore::testing::MemoryKeystore::new(), + )); ext.execute_with(|| { System::set_block_number(1); }); diff --git a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs index f4885b1b..878c0150 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs @@ -1,21 +1,62 @@ //! Tests for S3 Registry pallet. use crate::{mock::*, Error, S3Buckets}; +use codec::Encode; use frame_support::{assert_noop, assert_ok, traits::ConstU32, BoundedVec}; -use pallet_storage_provider::ProviderSettings; +use pallet_storage_provider::{AgreementTermsOf, ProviderSettings}; +use sp_core::crypto::KeyTypeId; +use storage_primitives::{AgreementTerms, BucketId}; +const PROVIDER_KEY_TYPE: KeyTypeId = KeyTypeId(*b"prov"); + +#[allow(dead_code)] fn test_public_key() -> BoundedVec> { vec![1u8; 32].try_into().unwrap() } -/// Register provider (account 3) with accepting_primary = true, capacity 200. -fn setup_provider() { +/// Generate a provider sr25519 keypair via the runtime keystore. +fn generate_provider_public_key( + seed: &str, +) -> (sp_core::sr25519::Public, BoundedVec>) { + let public = sp_io::crypto::sr25519_generate(PROVIDER_KEY_TYPE, Some(seed.as_bytes().to_vec())); + let bounded = public.0.to_vec().try_into().unwrap(); + (public, bounded) +} + +/// Sign SCALE-encoded terms with the provider's keystore key. +fn sign_terms( + public: &sp_core::sr25519::Public, + terms: &AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&terms.encode()); + let sig = sp_io::crypto::sr25519_sign(PROVIDER_KEY_TYPE, public, &hash) + .expect("keystore signs with a key it generated"); + sp_runtime::MultiSignature::Sr25519(sig) +} + +/// Build primary terms for the standard test provider. +fn primary_terms(owner: u64, max_bytes: u64, duration: u64, nonce: u64) -> AgreementTermsOf { + AgreementTerms { + owner, + max_bytes, + duration, + price_per_byte: 0u128, + valid_until: 1_000_000u64, + nonce, + replica_params: None, + } +} + +/// Register provider (account 3) with accepting_primary = true. Returns the +/// sr25519 public key it was registered with so callers can sign terms. +fn setup_provider() -> sp_core::sr25519::Public { let multiaddr: BoundedVec> = b"/ip4/127.0.0.1/tcp/3000".to_vec().try_into().unwrap(); + let (public, public_key_bytes) = generate_provider_public_key("//Provider"); assert_ok!(StorageProvider::register_provider( RuntimeOrigin::signed(3), multiaddr, - test_public_key(), + public_key_bytes, 10_000_000_000_000 // Must exceed MinProviderStake (1_000_000_000_000) )); let settings = ProviderSettings { @@ -31,15 +72,41 @@ fn setup_provider() { RuntimeOrigin::signed(3), settings )); + public +} + +/// Register provider (account 3) and open an S3 bucket "my-bucket" owned by +/// `owner` via the signed-terms path. Returns the S3 bucket id. +/// +/// Used by tests that just need *a* bucket to operate on (put/delete object, +/// delete bucket) and don't care about the agreement details. +fn setup_provider_and_s3_bucket(owner: u64, nonce: u64) -> u64 { + let provider_pk = setup_provider(); + let terms = primary_terms(owner, 100, 500, nonce); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(S3Registry::create_s3_bucket( + RuntimeOrigin::signed(owner), + b"my-bucket".to_vec(), + 3, + terms, + sig, + )); + 0 } #[test] fn create_s3_bucket_works() { new_test_ext().execute_with(|| { + let provider_pk = setup_provider(); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(S3Registry::create_s3_bucket( RuntimeOrigin::signed(1), b"my-bucket".to_vec(), - 1, // min_providers + 3, + terms, + sig, )); let bucket = S3Buckets::::get(0).unwrap(); @@ -47,40 +114,69 @@ fn create_s3_bucket_works() { assert_eq!(bucket.owner, 1); assert_eq!(bucket.object_count, 0); assert_eq!(bucket.total_size, 0); - // Layer 0 bucket should have been created automatically - assert!(pallet_storage_provider::Buckets::::get(bucket.layer0_bucket_id).is_some()); + + // Layer 0 bucket should exist with the named provider as the lone + // primary, and the agreement was opened atomically. + let l0_bucket = + pallet_storage_provider::Buckets::::get(bucket.layer0_bucket_id).unwrap(); + assert_eq!(l0_bucket.min_providers, 1); + assert_eq!(l0_bucket.primary_providers.to_vec(), vec![3]); + let bid: BucketId = bucket.layer0_bucket_id; + assert!(pallet_storage_provider::StorageAgreements::::contains_key(bid, 3)); }); } #[test] -fn create_s3_bucket_sets_min_providers() { +fn create_s3_bucket_surfaces_layer0_signature_errors() { + // If the named provider isn't registered, signature verification fails + // at Layer 0 and that error surfaces directly through the S3 registry — + // no custom NoProvidersAvailable / AgreementRequestFailed shim wraps it. new_test_ext().execute_with(|| { - assert_ok!(S3Registry::create_s3_bucket( - RuntimeOrigin::signed(1), - b"multi-provider-bucket".to_vec(), - 2, // min_providers - )); - - let bucket = S3Buckets::::get(0).unwrap(); - // Layer 0 bucket should have min_providers set - let l0_bucket = - pallet_storage_provider::Buckets::::get(bucket.layer0_bucket_id).unwrap(); - assert_eq!(l0_bucket.min_providers, 2); + let (unregistered_pk, _) = generate_provider_public_key("//Ghost"); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&unregistered_pk, &terms); + assert_noop!( + S3Registry::create_s3_bucket( + RuntimeOrigin::signed(1), + b"my-bucket".to_vec(), + 3, + terms, + sig, + ), + pallet_storage_provider::Error::::ProviderNotFound + ); }); } #[test] fn create_s3_bucket_fails_invalid_name() { new_test_ext().execute_with(|| { - // Too short + let provider_pk = setup_provider(); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&provider_pk, &terms); + + // Too short — the S3 layer's name validation runs before Layer 0 + // so the nonce isn't consumed. assert_noop!( - S3Registry::create_s3_bucket(RuntimeOrigin::signed(1), b"ab".to_vec(), 1,), + S3Registry::create_s3_bucket( + RuntimeOrigin::signed(1), + b"ab".to_vec(), + 3, + terms.clone(), + sig.clone(), + ), Error::::InvalidBucketName ); - // Uppercase not allowed + // Uppercase letters aren't allowed in S3 bucket names. assert_noop!( - S3Registry::create_s3_bucket(RuntimeOrigin::signed(1), b"MyBucket".to_vec(), 1,), + S3Registry::create_s3_bucket( + RuntimeOrigin::signed(1), + b"MyBucket".to_vec(), + 3, + terms, + sig, + ), Error::::InvalidBucketName ); }); @@ -89,14 +185,29 @@ fn create_s3_bucket_fails_invalid_name() { #[test] fn create_s3_bucket_fails_duplicate_name() { new_test_ext().execute_with(|| { + let provider_pk = setup_provider(); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&provider_pk, &terms); assert_ok!(S3Registry::create_s3_bucket( RuntimeOrigin::signed(1), b"my-bucket".to_vec(), - 1, + 3, + terms, + sig, )); + // Second attempt uses a fresh nonce so Layer 0 *would* accept it, + // but the S3 layer rejects the duplicate bucket name first. + let terms2 = primary_terms(1, 100, 500, 2); + let sig2 = sign_terms(&provider_pk, &terms2); assert_noop!( - S3Registry::create_s3_bucket(RuntimeOrigin::signed(1), b"my-bucket".to_vec(), 1,), + S3Registry::create_s3_bucket( + RuntimeOrigin::signed(1), + b"my-bucket".to_vec(), + 3, + terms2, + sig2, + ), Error::::BucketNameExists ); }); @@ -105,29 +216,21 @@ fn create_s3_bucket_fails_duplicate_name() { #[test] fn delete_s3_bucket_works() { new_test_ext().execute_with(|| { - assert_ok!(S3Registry::create_s3_bucket( + let s3_bucket_id = setup_provider_and_s3_bucket(1, 1); + assert_ok!(S3Registry::delete_s3_bucket( RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 1, + s3_bucket_id )); - - assert_ok!(S3Registry::delete_s3_bucket(RuntimeOrigin::signed(1), 0)); - - assert!(S3Buckets::::get(0).is_none()); + assert!(S3Buckets::::get(s3_bucket_id).is_none()); }); } #[test] fn delete_s3_bucket_fails_not_owner() { new_test_ext().execute_with(|| { - assert_ok!(S3Registry::create_s3_bucket( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 1, - )); - + let s3_bucket_id = setup_provider_and_s3_bucket(1, 1); assert_noop!( - S3Registry::delete_s3_bucket(RuntimeOrigin::signed(2), 0), + S3Registry::delete_s3_bucket(RuntimeOrigin::signed(2), s3_bucket_id), Error::::NotBucketOwner ); }); @@ -146,16 +249,11 @@ fn delete_s3_bucket_fails_not_found() { #[test] fn put_and_get_object_metadata_works() { new_test_ext().execute_with(|| { - assert_ok!(S3Registry::create_s3_bucket( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 1, - )); - + let s3_bucket_id = setup_provider_and_s3_bucket(1, 1); let cid = sp_core::H256::repeat_byte(0xAB); assert_ok!(S3Registry::put_object_metadata( RuntimeOrigin::signed(1), - 0, + s3_bucket_id, b"photos/cat.jpg".to_vec(), cid, 1024, @@ -163,13 +261,11 @@ fn put_and_get_object_metadata_works() { vec![], )); - // Check bucket stats updated - let bucket = S3Buckets::::get(0).unwrap(); + let bucket = S3Buckets::::get(s3_bucket_id).unwrap(); assert_eq!(bucket.object_count, 1); assert_eq!(bucket.total_size, 1024); - // Check object exists - let obj = S3Registry::get_object(0, b"photos/cat.jpg").unwrap(); + let obj = S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").unwrap(); assert_eq!(obj.cid, cid); assert_eq!(obj.size, 1024); }); @@ -178,16 +274,11 @@ fn put_and_get_object_metadata_works() { #[test] fn delete_object_metadata_works() { new_test_ext().execute_with(|| { - assert_ok!(S3Registry::create_s3_bucket( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 1, - )); - + let s3_bucket_id = setup_provider_and_s3_bucket(1, 1); let cid = sp_core::H256::repeat_byte(0xAB); assert_ok!(S3Registry::put_object_metadata( RuntimeOrigin::signed(1), - 0, + s3_bucket_id, b"photos/cat.jpg".to_vec(), cid, 1024, @@ -197,33 +288,25 @@ fn delete_object_metadata_works() { assert_ok!(S3Registry::delete_object_metadata( RuntimeOrigin::signed(1), - 0, + s3_bucket_id, b"photos/cat.jpg".to_vec(), )); - // Check bucket stats updated - let bucket = S3Buckets::::get(0).unwrap(); + let bucket = S3Buckets::::get(s3_bucket_id).unwrap(); assert_eq!(bucket.object_count, 0); assert_eq!(bucket.total_size, 0); - - // Check object removed - assert!(S3Registry::get_object(0, b"photos/cat.jpg").is_none()); + assert!(S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").is_none()); }); } #[test] fn delete_nonempty_bucket_fails() { new_test_ext().execute_with(|| { - assert_ok!(S3Registry::create_s3_bucket( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 1, - )); - + let s3_bucket_id = setup_provider_and_s3_bucket(1, 1); let cid = sp_core::H256::repeat_byte(0xAB); assert_ok!(S3Registry::put_object_metadata( RuntimeOrigin::signed(1), - 0, + s3_bucket_id, b"file.txt".to_vec(), cid, 100, @@ -232,100 +315,8 @@ fn delete_nonempty_bucket_fails() { )); assert_noop!( - S3Registry::delete_s3_bucket(RuntimeOrigin::signed(1), 0), + S3Registry::delete_s3_bucket(RuntimeOrigin::signed(1), s3_bucket_id), Error::::BucketNotEmpty ); }); } - -// --- create_s3_bucket_with_storage tests --- - -#[test] -fn create_s3_bucket_with_storage_works() { - new_test_ext().execute_with(|| { - setup_provider(); - - assert_ok!(S3Registry::create_s3_bucket_with_storage( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 100, // max_capacity - 500, // duration - 1000, // max_payment - )); - - let bucket = S3Buckets::::get(0).unwrap(); - assert_eq!(bucket.name.as_slice(), b"my-bucket"); - assert_eq!(bucket.owner, 1); - assert_eq!(bucket.object_count, 0); - assert_eq!(bucket.total_size, 0); - - // Layer 0 bucket should exist - assert!(pallet_storage_provider::Buckets::::get(bucket.layer0_bucket_id).is_some()); - - // Agreement should have been requested - let l0_bucket = - pallet_storage_provider::Buckets::::get(bucket.layer0_bucket_id).unwrap(); - assert_eq!(l0_bucket.min_providers, 1); - }); -} - -#[test] -fn create_s3_bucket_with_storage_fails_no_providers() { - new_test_ext().execute_with(|| { - // No providers registered - assert_noop!( - S3Registry::create_s3_bucket_with_storage( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 100, - 500, - 1000, - ), - Error::::NoProvidersAvailable - ); - }); -} - -#[test] -fn create_s3_bucket_with_storage_fails_invalid_name() { - new_test_ext().execute_with(|| { - setup_provider(); - - assert_noop!( - S3Registry::create_s3_bucket_with_storage( - RuntimeOrigin::signed(1), - b"ab".to_vec(), // too short - 100, - 500, - 1000, - ), - Error::::InvalidBucketName - ); - }); -} - -#[test] -fn create_s3_bucket_with_storage_fails_duplicate_name() { - new_test_ext().execute_with(|| { - setup_provider(); - - assert_ok!(S3Registry::create_s3_bucket_with_storage( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 100, - 500, - 1000, - )); - - assert_noop!( - S3Registry::create_s3_bucket_with_storage( - RuntimeOrigin::signed(1), - b"my-bucket".to_vec(), - 100, - 500, - 1000, - ), - Error::::BucketNameExists - ); - }); -} From fd0bbef4f31281c95343fee91f8b11ba0d88e279 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 18:27:05 +0700 Subject: [PATCH 08/44] feat: rewire pallet-drive-registry to signed-terms create_drive pallet-drive-registry - create_drive is updated following new flow. - allocate_bucket_for_user is removed. - Remove unnecessary code, update tests and benchmarks runtime - drop genesis bucket on Layer 0 --- Cargo.lock | 1 + runtime/src/genesis_config_presets.rs | 14 - .../src/genesis_config_presets.rs | 14 - .../file-system/pallet-registry/Cargo.toml | 3 + .../pallet-registry/src/bechmarking.rs | 187 +++++++------- .../file-system/pallet-registry/src/lib.rs | 175 ++----------- .../file-system/pallet-registry/src/mock.rs | 8 +- .../file-system/pallet-registry/src/tests.rs | 243 ++++++++++-------- .../file-system/primitives/src/lib.rs | 5 +- 9 files changed, 262 insertions(+), 388 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd70603a..4c06ef73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4536,6 +4536,7 @@ dependencies = [ "scale-info", "sp-core", "sp-io", + "sp-keystore", "sp-runtime", "storage-primitives", ] diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 24789283..cc9ff258 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -17,7 +17,6 @@ fn storage_parachain_genesis( endowment: Balance, id: ParaId, sudo_account: Option, - genesis_buckets: Vec<(AccountId, u32)>, ) -> serde_json::Value { build_struct_json_patch!(RuntimeGenesisConfig { balances: BalancesConfig { @@ -48,9 +47,6 @@ fn storage_parachain_genesis( safe_xcm_version: Some(xcm::latest::VERSION) }, sudo: SudoConfig { key: sudo_account }, - storage_provider: StorageProviderConfig { - buckets: genesis_buckets, - }, }) } @@ -76,11 +72,6 @@ pub fn get_preset(id: &PresetId) -> Option> { PARA_ID, // Sudo Some(Sr25519Keyring::Alice.to_account_id()), - // Genesis buckets: creates bucket_id=0 and bucket_id=1 (admin, min_providers) - vec![ - (Sr25519Keyring::Bob.to_account_id(), 1), - (Sr25519Keyring::Bob.to_account_id(), 1), - ], ), sp_genesis_builder::DEV_RUNTIME_PRESET => storage_parachain_genesis( // initial collators. @@ -98,11 +89,6 @@ pub fn get_preset(id: &PresetId) -> Option> { PARA_ID, // Sudo Some(Sr25519Keyring::Alice.to_account_id()), - // Genesis buckets: creates bucket_id=0 and bucket_id=1 (admin, min_providers) - vec![ - (Sr25519Keyring::Bob.to_account_id(), 1), - (Sr25519Keyring::Bob.to_account_id(), 1), - ], ), _ => return None, }; diff --git a/runtimes/web3-storage-paseo/src/genesis_config_presets.rs b/runtimes/web3-storage-paseo/src/genesis_config_presets.rs index 40c89a71..78237bec 100644 --- a/runtimes/web3-storage-paseo/src/genesis_config_presets.rs +++ b/runtimes/web3-storage-paseo/src/genesis_config_presets.rs @@ -17,7 +17,6 @@ fn storage_parachain_genesis( endowment: Balance, id: ParaId, sudo_account: Option, - genesis_buckets: Vec<(AccountId, u32)>, ) -> serde_json::Value { build_struct_json_patch!(RuntimeGenesisConfig { balances: BalancesConfig { @@ -48,9 +47,6 @@ fn storage_parachain_genesis( safe_xcm_version: Some(xcm::latest::VERSION) }, sudo: SudoConfig { key: sudo_account }, - storage_provider: StorageProviderConfig { - buckets: genesis_buckets, - }, }) } @@ -76,11 +72,6 @@ pub fn get_preset(id: &PresetId) -> Option> { WEB3_STORAGE_PARA_ID, // Sudo Some(Sr25519Keyring::Alice.to_account_id()), - // Genesis buckets: creates bucket_id=0 and bucket_id=1 (admin, min_providers) - vec![ - (Sr25519Keyring::Bob.to_account_id(), 1), - (Sr25519Keyring::Bob.to_account_id(), 1), - ], ), sp_genesis_builder::DEV_RUNTIME_PRESET => storage_parachain_genesis( // initial collators. @@ -98,11 +89,6 @@ pub fn get_preset(id: &PresetId) -> Option> { WEB3_STORAGE_PARA_ID, // Sudo Some(Sr25519Keyring::Alice.to_account_id()), - // Genesis buckets: creates bucket_id=0 and bucket_id=1 (admin, min_providers) - vec![ - (Sr25519Keyring::Bob.to_account_id(), 1), - (Sr25519Keyring::Bob.to_account_id(), 1), - ], ), _ => return None, }; diff --git a/storage-interfaces/file-system/pallet-registry/Cargo.toml b/storage-interfaces/file-system/pallet-registry/Cargo.toml index 182cab79..5bd2d460 100644 --- a/storage-interfaces/file-system/pallet-registry/Cargo.toml +++ b/storage-interfaces/file-system/pallet-registry/Cargo.toml @@ -23,6 +23,9 @@ file-system-primitives = { workspace = true } pallet-storage-provider = { workspace = true } storage-primitives = { workspace = true } +[dev-dependencies] +sp-keystore = { workspace = true, features = ["std"] } + [features] default = ["std"] std = [ diff --git a/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs b/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs index daeec8c4..a20e1aa6 100644 --- a/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs +++ b/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs @@ -4,12 +4,13 @@ //! most expensive control flow inside the extrinsic so that generated weights //! are upper bounds: //! -//! * `create_drive` — request 1 primary + `MaxPrimaryProviders - 1` replica -//! agreements, iterate the full provider set, write a max-length name, and -//! `try_push` into a near-full `UserDrives` bounded vec. -//! * `delete_drive` — bucket has `MaxPrimaryProviders` accepted agreements -//! AND `MaxMembers` members, so `cleanup_bucket_internal` runs the prorated -//! refund loop and the per-member `MemberBuckets` retain at their maxima. +//! * `create_drive` — provider with full max-capacity stake (so Layer 0 runs +//! signature / replay / capacity / stake / duration / price), write a +//! max-length name, and `try_push` into a near-full `UserDrives` bounded +//! vec. +//! * `delete_drive` — bucket has one accepted primary agreement AND +//! `MaxMembers` members, so `cleanup_bucket_internal` runs both refund and +//! per-member retain loops; caller's `UserDrives` is at `MaxDrivesPerUser`. //! * `share_drive` — bucket sits at `MaxMembers - 1`, forcing //! `set_member_internal` to scan the full list and `try_push` at the //! capacity boundary. @@ -26,12 +27,15 @@ use frame_support::{ BoundedVec, }; use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; -use pallet_storage_provider::{Pallet as StorageProvider, ProviderSettings}; +use pallet_storage_provider::{AgreementTermsOf, Pallet as StorageProvider, ProviderSettings}; use sp_runtime::traits::{Bounded, SaturatedConversion}; -use storage_primitives::Role; +use storage_primitives::{AgreementTerms, Role}; const SEED: u32 = 0; +/// Key type used by the benchmarking keystore for provider signing material. +const KEY_TYPE: sp_core::crypto::KeyTypeId = sp_core::crypto::KeyTypeId(*b"drbn"); + /// Create an account with effectively unbounded balance. fn funded_account(name: &'static str, index: u32) -> T::AccountId { let account: T::AccountId = account(name, index, SEED); @@ -41,13 +45,18 @@ fn funded_account(name: &'static str, index: u32) -> T::AccountId { account } -/// Register a storage provider that accepts both primary and replica -/// agreements with enough stake and capacity to back every agreement that -/// these benchmarks open against it. -fn create_provider(index: u32) -> T::AccountId { +/// Register a storage provider that accepts primary agreements with enough +/// stake and capacity to back the benchmarks. Returns both the account and +/// the sr25519 public key registered against it so the `create_drive` +/// benchmark can sign terms with the matching keystore key. +fn create_provider(index: u32) -> (T::AccountId, sp_core::sr25519::Public) { let provider = funded_account::("provider", index); let multiaddr = b"/ip4/127.0.0.1/tcp/3000".to_vec(); - let public_key = [0u8; 32].to_vec(); + + // Generate an sr25519 key in the runtime keystore so the pallet can + // verify signatures over agreement terms. + let seed = alloc::format!("//DriveBenchProvider{index}"); + let public_key = sp_io::crypto::sr25519_generate(KEY_TYPE, Some(seed.into_bytes())); // Stake must cover declared capacity at MinStakePerByte. let capacity: u64 = 1_000_000_000; @@ -58,7 +67,7 @@ fn create_provider(index: u32) -> T::AccountId { let _ = StorageProvider::::register_provider( RawOrigin::Signed(provider.clone()).into(), multiaddr.try_into().unwrap(), - public_key.try_into().unwrap(), + public_key.0.to_vec().try_into().unwrap(), stake, ); @@ -75,13 +84,53 @@ fn create_provider(index: u32) -> T::AccountId { }, ); - provider + (provider, public_key) +} + +/// Sign agreement terms with the provider's keystore key. +fn sign_terms( + public_key: &sp_core::sr25519::Public, + terms: &AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&codec::Encode::encode(terms)); + let sig = sp_io::crypto::sr25519_sign(KEY_TYPE, public_key, &hash) + .expect("benchmarking keystore signs with a key it generated"); + sp_runtime::MultiSignature::Sr25519(sig) +} + +/// Build primary terms with the standard benchmark shape. +fn make_primary_terms(owner: &T::AccountId, nonce: u64) -> AgreementTermsOf { + AgreementTerms { + owner: owner.clone(), + max_bytes: 1_000u64, + duration: 100u32.into(), + price_per_byte: 1u32.into(), + valid_until: BlockNumberFor::::max_value(), + nonce, + replica_params: None, + } } -/// `min_providers` is `Option` in `create_drive`, so we clamp the -/// `MaxPrimaryProviders` config (typed `u32`) to `u8::MAX` before passing it. -fn max_providers_u8() -> u8 { - (T::MaxPrimaryProviders::get() as u64).min(u8::MAX as u64) as u8 +/// Open a drive for `user` via the signed-terms path; returns +/// `(drive_id, bucket_id)`. +fn create_drive_for( + user: &T::AccountId, + provider: &T::AccountId, + provider_pk: &sp_core::sr25519::Public, + nonce: u64, +) -> (DriveId, u64) { + let terms = make_primary_terms::(user, nonce); + let sig = sign_terms::(provider_pk, &terms); + let _ = DriveRegistry::::create_drive( + RawOrigin::Signed(user.clone()).into(), + None, + provider.clone(), + terms, + sig, + ); + let drive_id = NextDriveId::::get().saturating_sub(1); + let drive = Drives::::get(drive_id).expect("create_drive just inserted this"); + (drive_id, drive.bucket_id) } /// Pre-fill the user's drive list to `MaxDrivesPerUser - 1` so the bounded @@ -105,88 +154,46 @@ mod benchmarks { // ───────────────────────────────────────────────────────────────────────── /// Worst case: - /// - `min_providers = MaxPrimaryProviders` → request 1 primary AND - /// `n - 1` replica agreements (the replica branch executes fully). - /// - `n` providers registered → `query_available_providers` iterates the - /// full set for both the primary and replica searches. + /// - Provider with full max-capacity stake registered so the Layer 0 + /// signature / replay / capacity / stake / duration / price checks all + /// run. /// - `name` is at `MaxDriveNameLength` so the bounded-vec conversion and /// storage write are at maximum size. /// - `UserDrives` for the caller is pre-filled to `MaxDrivesPerUser - 1` /// so the bounded `try_push` runs at the capacity boundary. #[benchmark] fn create_drive() { - let n = T::MaxPrimaryProviders::get(); - for i in 0..n { - let _ = create_provider::(i); - } + let (provider, provider_pk) = create_provider::(0); let user = funded_account::("user", 0); prefill_user_drives::(&user); let name = vec![b'x'; T::MaxDriveNameLength::get() as usize]; - let max_capacity: u64 = 1_000; - let storage_period: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - let min_providers = max_providers_u8::(); + let terms = make_primary_terms::(&user, 1); + let sig = sign_terms::(&provider_pk, &terms); #[extrinsic_call] - create_drive( - RawOrigin::Signed(user), - Some(name), - max_capacity, - storage_period, - payment, - Some(min_providers), - ); + create_drive(RawOrigin::Signed(user), Some(name), provider, terms, sig); } /// Worst case for `cleanup_bucket_internal`: - /// - `MaxPrimaryProviders` storage agreements have been accepted, so the - /// refund/transfer/event loop runs the maximum number of times. + /// - One accepted primary agreement (the establish-flow path). /// - Bucket has `MaxMembers` members, so the per-member `MemberBuckets` /// retain loop runs at its maximum. /// - Caller's `UserDrives` is at `MaxDrivesPerUser`, so the `retain` /// that removes the deleted drive scans the full list. #[benchmark] fn delete_drive() { - let n = T::MaxPrimaryProviders::get(); - let providers: Vec = (0..n).map(create_provider::).collect(); - + let (provider, provider_pk) = create_provider::(0); let user = funded_account::("user", 0); - let name = vec![b'x'; T::MaxDriveNameLength::get() as usize]; - let max_capacity: u64 = 1_000; - let storage_period: BlockNumberFor = 100u32.into(); - let payment = BalanceOf::::max_value() / 10u32.into(); - let min_providers = max_providers_u8::(); - - // Create the drive — opens primary + (n - 1) replica agreement requests. - let _ = DriveRegistry::::create_drive( - RawOrigin::Signed(user.clone()).into(), - Some(name), - max_capacity, - storage_period, - payment, - Some(min_providers), - ); - let drive_id = NextDriveId::::get().saturating_sub(1); - let drive = Drives::::get(drive_id).expect("create_drive just inserted this"); - - // Each provider accepts so cleanup iterates the full StorageAgreements set - // (not the pending-request set). - for provider in &providers { - let _ = StorageProvider::::accept_agreement( - RawOrigin::Signed(provider.clone()).into(), - drive.bucket_id, - ); - } + let (drive_id, bucket_id) = create_drive_for::(&user, &provider, &provider_pk, 1); // Fill members up to MaxMembers (owner is already a member). let max_members = ::MaxMembers::get(); for i in 0..max_members.saturating_sub(1) { let m = funded_account::("member", i); - let _ = - StorageProvider::::set_member_internal(&user, drive.bucket_id, m, Role::Reader); + let _ = StorageProvider::::set_member_internal(&user, bucket_id, m, Role::Reader); } // Pre-fill UserDrives to the maximum so the `retain` after deletion @@ -215,26 +222,15 @@ mod benchmarks { /// - The push happens right at the capacity boundary. #[benchmark] fn share_drive() { - let _ = create_provider::(0); + let (provider, provider_pk) = create_provider::(0); let user = funded_account::("user", 0); - - let _ = DriveRegistry::::create_drive( - RawOrigin::Signed(user.clone()).into(), - None, - 1_000u64, - 100u32.into(), - BalanceOf::::max_value() / 10u32.into(), - Some(1), - ); - let drive_id = NextDriveId::::get().saturating_sub(1); - let drive = Drives::::get(drive_id).expect("create_drive just inserted this"); + let (drive_id, bucket_id) = create_drive_for::(&user, &provider, &provider_pk, 1); // Push members up to MaxMembers - 1 (owner counts as the first member). let max_members = ::MaxMembers::get(); for i in 0..max_members.saturating_sub(2) { let m = funded_account::("filler", i); - let _ = - StorageProvider::::set_member_internal(&user, drive.bucket_id, m, Role::Reader); + let _ = StorageProvider::::set_member_internal(&user, bucket_id, m, Role::Reader); } let new_member = funded_account::("new_member", 0); @@ -249,31 +245,20 @@ mod benchmarks { /// we remove the last non-admin entry. #[benchmark] fn unshare_drive() { - let _ = create_provider::(0); + let (provider, provider_pk) = create_provider::(0); let user = funded_account::("user", 0); - - let _ = DriveRegistry::::create_drive( - RawOrigin::Signed(user.clone()).into(), - None, - 1_000u64, - 100u32.into(), - BalanceOf::::max_value() / 10u32.into(), - Some(1), - ); - let drive_id = NextDriveId::::get().saturating_sub(1); - let drive = Drives::::get(drive_id).expect("create_drive just inserted this"); + let (drive_id, bucket_id) = create_drive_for::(&user, &provider, &provider_pk, 1); // Fill the bucket to MaxMembers, with the removal target inserted last. let max_members = ::MaxMembers::get(); for i in 0..max_members.saturating_sub(2) { let m = funded_account::("filler", i); - let _ = - StorageProvider::::set_member_internal(&user, drive.bucket_id, m, Role::Reader); + let _ = StorageProvider::::set_member_internal(&user, bucket_id, m, Role::Reader); } let target = funded_account::("target", 0); let _ = StorageProvider::::set_member_internal( &user, - drive.bucket_id, + bucket_id, target.clone(), Role::Reader, ); diff --git a/storage-interfaces/file-system/pallet-registry/src/lib.rs b/storage-interfaces/file-system/pallet-registry/src/lib.rs index ef134325..54db39ad 100644 --- a/storage-interfaces/file-system/pallet-registry/src/lib.rs +++ b/storage-interfaces/file-system/pallet-registry/src/lib.rs @@ -99,7 +99,7 @@ pub mod pallet { _, Blake2_128Concat, DriveId, - DriveInfo, T::MaxDriveNameLength, BalanceOf>, + DriveInfo, T::MaxDriveNameLength>, >; /// User's drives (account -> list of drive IDs) @@ -161,22 +161,8 @@ pub mod pallet { DriveNameTooLong, /// Drive ID overflow DriveIdOverflow, - /// Invalid storage size (must be > 0) - InvalidStorageSize, - /// Invalid provider count (must be > 0) - InvalidProviderCount, - /// Invalid storage period (must be > 0) - InvalidStoragePeriod, - /// Invalid payment amount (must be > 0) - InvalidPayment, - /// Failed to create bucket in Layer 0 - BucketCreationFailed, /// Failed to cleanup bucket in Layer 0 BucketCleanupFailed, - /// No storage providers available - NoProvidersAvailable, - /// Insufficient replica providers available - InsufficientReplicaProviders, /// Not authorized to share this drive (must be owner or bucket admin) NotAuthorizedToShare, /// Failed to update bucket membership in Layer 0 @@ -187,37 +173,30 @@ pub mod pallet { impl Pallet { /// Create a new drive with automatic bucket creation /// - /// The system automatically creates a Layer 0 bucket, selects providers, - /// and establishes storage agreements. File/directory metadata is managed - /// off-chain by the provider node. + /// Atomically opens the Layer 0 bucket + primary storage agreement + /// (via `establish_storage_agreement_internal`) and records the + /// drive metadata on top. The caller obtains `terms` and `sig` + /// off-chain from the provider; Layer 0 enforces signature, replay + /// window, and capacity/stake/duration/price checks — those errors + /// surface directly so the caller can react to them. + /// /// /// Parameters: /// - `name`: Optional human-readable name for the drive - /// - `max_capacity`: Maximum storage capacity in bytes - /// - `storage_period`: Storage duration in blocks - /// - `payment`: Upfront payment tokens for storage agreements - /// - `min_providers`: Optional minimum number of providers (default: auto) + /// - `provider`: Provider account that signed the terms. + /// - `terms`: Provider-signed agreement terms. + /// - `sig`: Provider signature over the SCALE-encoded terms. #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::create_drive())] pub fn create_drive( origin: OriginFor, name: Option>, - max_capacity: u64, - storage_period: BlockNumberFor, - payment: BalanceOf, - min_providers: Option, + provider: T::AccountId, + terms: pallet_storage_provider::AgreementTermsOf, + sig: sp_runtime::MultiSignature, ) -> DispatchResult { let who = ensure_signed(origin)?; - // Validate inputs - ensure!(max_capacity > 0, Error::::InvalidStorageSize); - ensure!( - storage_period > BlockNumberFor::::from(0u32), - Error::::InvalidStoragePeriod - ); - use sp_runtime::traits::Zero; - ensure!(!payment.is_zero(), Error::::InvalidPayment); - // Convert name to BoundedVec let bounded_name = if let Some(n) = name { Some(BoundedVec::try_from(n).map_err(|_| Error::::DriveNameTooLong)?) @@ -232,14 +211,18 @@ pub mod pallet { Error::::TooManyDrives ); - // Allocate bucket with storage parameters - let bucket_id = Self::allocate_bucket_for_user( - &who, - max_capacity, - storage_period, - payment, - min_providers, - )?; + // Snapshot the values the DriveInfo wants before handing `terms` + // to Layer 0 (which consumes it). + let max_capacity = terms.max_bytes; + let storage_period = terms.duration; + + // Open the Layer 0 bucket + primary agreement atomically. + // Layer 0 errors (bad signature, replay, capacity, price, …) + // surface directly via `?`. + let bucket_id = + pallet_storage_provider::Pallet::::establish_storage_agreement_internal( + &who, &provider, terms, &sig, + )?; // Get next drive ID let drive_id = NextDriveId::::get(); @@ -258,7 +241,6 @@ pub mod pallet { max_capacity, storage_period, expires_at, - payment, }; // Store drive @@ -403,114 +385,11 @@ pub mod pallet { } impl Pallet { - /// Allocate a bucket for a user with specified storage requirements. - fn allocate_bucket_for_user( - user: &T::AccountId, - max_capacity: u64, - storage_period: BlockNumberFor, - payment: BalanceOf, - min_providers: Option, - ) -> Result> { - use sp_runtime::traits::{CheckedDiv, SaturatedConversion, Zero}; - - // Determine number of providers - let num_providers: u8 = if let Some(min) = min_providers { - ensure!(min > 0, Error::::InvalidProviderCount); - min - } else { - let threshold_blocks = BlockNumberFor::::from(1000u32); - if storage_period > threshold_blocks { - 3 - } else { - 1 - } - }; - - // Step 1: Create bucket in Layer 0 - let bucket_id = pallet_storage_provider::Pallet::::create_bucket_internal( - user, - num_providers as u32, - ) - .map_err(|_| Error::::BucketCreationFailed)?; - - // Step 2: Calculate payment per provider - let divisor: BalanceOf = (num_providers as u32).saturated_into(); - let payment_per_provider = payment - .checked_div(&divisor) - .ok_or(Error::::BucketCreationFailed)?; - - // Step 3: Find and request primary agreement - let available_primary_providers = - pallet_storage_provider::Pallet::::query_available_providers(max_capacity, true); - - ensure!( - !available_primary_providers.is_empty(), - Error::::NoProvidersAvailable - ); - - let primary_provider = &available_primary_providers[0]; - - pallet_storage_provider::Pallet::::request_primary_agreement_internal( - user, - bucket_id, - primary_provider, - max_capacity, - storage_period, - payment_per_provider, - ) - .map_err(|_| Error::::BucketCreationFailed)?; - - // Step 4: Request replica agreements (if needed) - if num_providers > 1 { - let available_replica_providers = - pallet_storage_provider::Pallet::::query_available_providers( - max_capacity, - false, - ); - - let num_replicas = (num_providers - 1) as usize; - ensure!( - available_replica_providers.len() >= num_replicas, - Error::::InsufficientReplicaProviders - ); - - let mut replica_count = 0; - for replica_provider in available_replica_providers.iter() { - if replica_count >= num_replicas { - break; - } - if replica_provider == primary_provider { - continue; - } - - let divisor_ten: BalanceOf = 10u32.saturated_into(); - let sync_balance = payment_per_provider - .checked_div(&divisor_ten) - .unwrap_or_else(Zero::zero); - - pallet_storage_provider::Pallet::::request_replica_agreement_internal( - user, - bucket_id, - replica_provider, - max_capacity, - storage_period, - payment_per_provider, - sync_balance, - ) - .map_err(|_| Error::::BucketCreationFailed)?; - - replica_count += 1; - } - } - - Ok(bucket_id) - } - /// Helper: Get drive info #[allow(clippy::type_complexity)] pub fn get_drive( drive_id: DriveId, - ) -> Option, T::MaxDriveNameLength, BalanceOf>> + ) -> Option, T::MaxDriveNameLength>> { Drives::::get(drive_id) } diff --git a/storage-interfaces/file-system/pallet-registry/src/mock.rs b/storage-interfaces/file-system/pallet-registry/src/mock.rs index 33b5cee1..12ce2cd8 100644 --- a/storage-interfaces/file-system/pallet-registry/src/mock.rs +++ b/storage-interfaces/file-system/pallet-registry/src/mock.rs @@ -139,5 +139,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .assimilate_storage(&mut t) .unwrap(); - t.into() + let mut ext: sp_io::TestExternalities = t.into(); + // Required so signed-terms tests can call `sr25519_sign` through the + // keystore extension. + ext.register_extension(sp_keystore::KeystoreExt::new( + sp_keystore::testing::MemoryKeystore::new(), + )); + ext } diff --git a/storage-interfaces/file-system/pallet-registry/src/tests.rs b/storage-interfaces/file-system/pallet-registry/src/tests.rs index 4c8025aa..a223daf4 100644 --- a/storage-interfaces/file-system/pallet-registry/src/tests.rs +++ b/storage-interfaces/file-system/pallet-registry/src/tests.rs @@ -1,82 +1,125 @@ -use crate::{mock::*, Error}; -use frame_support::assert_noop; -use storage_primitives::Role; - -#[test] -fn create_drive_validates_inputs() { - new_test_ext().execute_with(|| { - let alice = 1u64; +use crate::{ + mock::{*, MaxMultiaddrLength}, + Drives, Error, +}; +use codec::Encode; +use frame_support::{assert_noop, assert_ok, traits::ConstU32, BoundedVec}; +use pallet_storage_provider::{AgreementTermsOf, ProviderSettings}; +use sp_core::crypto::KeyTypeId; +use storage_primitives::{AgreementTerms, Role}; + +const PROVIDER_KEY_TYPE: KeyTypeId = KeyTypeId(*b"prov"); + +/// Generate a provider sr25519 keypair via the runtime keystore. +fn generate_provider_public_key( + seed: &str, +) -> (sp_core::sr25519::Public, BoundedVec>) { + let public = sp_io::crypto::sr25519_generate(PROVIDER_KEY_TYPE, Some(seed.as_bytes().to_vec())); + let bounded = public.0.to_vec().try_into().unwrap(); + (public, bounded) +} - // Zero capacity - assert_noop!( - DriveRegistry::create_drive( - RuntimeOrigin::signed(alice), - Some(b"My Drive".to_vec()), - 0, // invalid - 500, - 1_000_000_000_000, - None, - ), - Error::::InvalidStorageSize - ); +/// Sign SCALE-encoded terms with the provider's keystore key. +fn sign_terms( + public: &sp_core::sr25519::Public, + terms: &AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&terms.encode()); + let sig = sp_io::crypto::sr25519_sign(PROVIDER_KEY_TYPE, public, &hash) + .expect("keystore signs with a key it generated"); + sp_runtime::MultiSignature::Sr25519(sig) +} - // Zero storage period - assert_noop!( - DriveRegistry::create_drive( - RuntimeOrigin::signed(alice), - Some(b"My Drive".to_vec()), - 10_000_000_000, - 0, // invalid - 1_000_000_000_000, - None, - ), - Error::::InvalidStoragePeriod - ); +/// Build primary terms for the standard test provider. +fn primary_terms(owner: u64, max_bytes: u64, duration: u64, nonce: u64) -> AgreementTermsOf { + AgreementTerms { + owner, + max_bytes, + duration, + price_per_byte: 0u128, + valid_until: 1_000_000u64, + nonce, + replica_params: None, + } +} - // Zero payment - assert_noop!( - DriveRegistry::create_drive( - RuntimeOrigin::signed(alice), - Some(b"My Drive".to_vec()), - 10_000_000_000, - 500, - 0, // invalid - None, - ), - Error::::InvalidPayment - ); +/// Register provider account 3 with capacity that covers our test agreements, +/// and return its sr25519 public key so callers can sign terms. +fn setup_provider() -> sp_core::sr25519::Public { + let multiaddr: BoundedVec = + b"/ip4/127.0.0.1/tcp/3000".to_vec().try_into().unwrap(); + let (public, public_key_bytes) = generate_provider_public_key("//Provider"); + assert_ok!(StorageProvider::register_provider( + RuntimeOrigin::signed(3), + multiaddr, + public_key_bytes, + 10_000_000_000_000, // Must exceed MinProviderStake (1_000_000_000_000) + )); + let settings = ProviderSettings { + min_duration: 10u64, + max_duration: 10_000u64, + price_per_byte: 0u128, + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 10_000_000_000, // stake / MinStakePerByte + }; + assert_ok!(StorageProvider::update_provider_settings( + RuntimeOrigin::signed(3), + settings + )); + public +} - // Zero min_providers - assert_noop!( - DriveRegistry::create_drive( - RuntimeOrigin::signed(alice), - Some(b"My Drive".to_vec()), - 10_000_000_000, - 500, - 1_000_000_000_000, - Some(0), // invalid - ), - Error::::InvalidProviderCount - ); +#[test] +fn create_drive_works() { + new_test_ext().execute_with(|| { + let provider_pk = setup_provider(); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&provider_pk, &terms); + + assert_ok!(DriveRegistry::create_drive( + RuntimeOrigin::signed(1), + Some(b"My Documents".to_vec()), + 3, + terms, + sig, + )); + + let drive = Drives::::get(0).unwrap(); + assert_eq!(drive.owner, 1); + assert_eq!(drive.max_capacity, 100); + assert_eq!(drive.storage_period, 500); + assert_eq!(drive.name.as_ref().map(|n| n.to_vec()), Some(b"My Documents".to_vec())); + + // Layer 0 bucket exists with the provider as the lone primary. + let l0_bucket = pallet_storage_provider::Buckets::::get(drive.bucket_id).unwrap(); + assert_eq!(l0_bucket.primary_providers.to_vec(), vec![3]); + assert!(pallet_storage_provider::StorageAgreements::::contains_key( + drive.bucket_id, + 3, + )); }); } #[test] -fn create_drive_fails_without_providers() { +fn create_drive_surfaces_layer0_signature_errors() { + // If the named provider isn't registered, signature verification fails + // at Layer 0 and surfaces directly — the registry no longer wraps it + // in NoProvidersAvailable / BucketCreationFailed. new_test_ext().execute_with(|| { - let alice = 1u64; - - // No providers registered in the test mock + let (unregistered_pk, _) = generate_provider_public_key("//Ghost"); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&unregistered_pk, &terms); assert_noop!( DriveRegistry::create_drive( - RuntimeOrigin::signed(alice), + RuntimeOrigin::signed(1), Some(b"My Documents".to_vec()), - 10_000_000_000, - 500, - 1_000_000_000_000, - None, + 3, + terms, + sig, ), - Error::::NoProvidersAvailable + pallet_storage_provider::Error::::ProviderNotFound ); }); } @@ -84,17 +127,18 @@ fn create_drive_fails_without_providers() { #[test] fn create_drive_name_too_long_fails() { new_test_ext().execute_with(|| { - let alice = 1u64; - let long_name = vec![b'a'; 257]; // Max is 256 + let provider_pk = setup_provider(); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&provider_pk, &terms); + let long_name = vec![b'a'; 257]; // MaxDriveNameLength = 256 in mock assert_noop!( DriveRegistry::create_drive( - RuntimeOrigin::signed(alice), + RuntimeOrigin::signed(1), Some(long_name), - 10_000_000_000, - 500, - 1_000_000_000_000, - None, + 3, + terms, + sig, ), Error::::DriveNameTooLong ); @@ -104,10 +148,8 @@ fn create_drive_name_too_long_fails() { #[test] fn delete_drive_not_found_fails() { new_test_ext().execute_with(|| { - let alice = 1u64; - assert_noop!( - DriveRegistry::delete_drive(RuntimeOrigin::signed(alice), 999), + DriveRegistry::delete_drive(RuntimeOrigin::signed(1), 999), Error::::DriveNotFound ); }); @@ -116,14 +158,20 @@ fn delete_drive_not_found_fails() { #[test] fn delete_drive_not_owner_fails() { new_test_ext().execute_with(|| { - // We can't easily create a drive without providers, so we just test that - // deleting a nonexistent drive gives DriveNotFound. - // A full integration test would set up providers + create drive + delete. - let bob = 2u64; - + let provider_pk = setup_provider(); + let terms = primary_terms(1, 100, 500, 1); + let sig = sign_terms(&provider_pk, &terms); + assert_ok!(DriveRegistry::create_drive( + RuntimeOrigin::signed(1), + None, + 3, + terms, + sig, + )); + // Bob is not the owner. assert_noop!( - DriveRegistry::delete_drive(RuntimeOrigin::signed(bob), 0), - Error::::DriveNotFound + DriveRegistry::delete_drive(RuntimeOrigin::signed(2), 0), + Error::::NotDriveOwner ); }); } @@ -152,16 +200,8 @@ fn helper_functions_work() { #[test] fn share_drive_fails_when_drive_not_found() { new_test_ext().execute_with(|| { - let alice = 1u64; - let bob = 2u64; - assert_noop!( - DriveRegistry::share_drive( - RuntimeOrigin::signed(alice), - 999, // nonexistent - bob, - Role::Reader, - ), + DriveRegistry::share_drive(RuntimeOrigin::signed(1), 999, 2, Role::Reader), Error::::DriveNotFound ); }); @@ -170,15 +210,8 @@ fn share_drive_fails_when_drive_not_found() { #[test] fn unshare_drive_fails_when_drive_not_found() { new_test_ext().execute_with(|| { - let alice = 1u64; - let bob = 2u64; - assert_noop!( - DriveRegistry::unshare_drive( - RuntimeOrigin::signed(alice), - 999, // nonexistent - bob, - ), + DriveRegistry::unshare_drive(RuntimeOrigin::signed(1), 999, 2), Error::::DriveNotFound ); }); @@ -187,13 +220,11 @@ fn unshare_drive_fails_when_drive_not_found() { #[test] fn share_drive_fails_when_non_owner_non_admin() { new_test_ext().execute_with(|| { - // Without a drive existing, we just test drive-not-found. - // Full permission tests require a registered provider + created drive. - let bob = 2u64; - let charlie = 3u64; - + // Without a drive existing, we just hit DriveNotFound. A full + // permission test would set up a drive and have a non-admin try + // to share it. assert_noop!( - DriveRegistry::share_drive(RuntimeOrigin::signed(bob), 0, charlie, Role::Writer,), + DriveRegistry::share_drive(RuntimeOrigin::signed(2), 0, 3, Role::Writer), Error::::DriveNotFound ); }); diff --git a/storage-interfaces/file-system/primitives/src/lib.rs b/storage-interfaces/file-system/primitives/src/lib.rs index 91a9b4e1..083ae74e 100644 --- a/storage-interfaces/file-system/primitives/src/lib.rs +++ b/storage-interfaces/file-system/primitives/src/lib.rs @@ -609,8 +609,7 @@ pub enum FileSystemError { pub struct DriveInfo< AccountId: Encode + Decode + MaxEncodedLen, BlockNumber: Encode + Decode + MaxEncodedLen, - MaxNameLength: Get, - Balance: Encode + Decode + MaxEncodedLen, + MaxNameLength: Get > { /// Owner of the drive pub owner: AccountId, @@ -626,8 +625,6 @@ pub struct DriveInfo< pub storage_period: BlockNumber, /// Expiry block number (created_at + storage_period) pub expires_at: BlockNumber, - /// Payment tokens for storage - pub payment: Balance, } // ============================================================================ From a3fbf60257ac4984ebe339f29c8f326a5839ffc8 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 28 May 2026 19:17:06 +0700 Subject: [PATCH 09/44] chore: update weights --- pallet/src/lib.rs | 4 +- pallet/src/weights.rs | 641 +++++++----------- .../src/weights/pallet_storage_provider.rs | 54 +- .../src/weights/pallet_drive_registry.rs | 62 +- .../src/weights/pallet_s3_registry.rs | 54 +- .../src/weights/pallet_storage_provider.rs | 302 +++------ runtimes/web3-storage-paseo/tests/tests.rs | 619 +++++++---------- .../pallet-registry/src/weights.rs | 130 ++-- .../s3/pallet-s3-registry/src/weights.rs | 125 ++-- 9 files changed, 785 insertions(+), 1206 deletions(-) diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 02e4e0f9..88c70c6f 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -1186,7 +1186,7 @@ pub mod pallet { /// the standard provider/capacity/stake checks and opens the /// agreement. #[pallet::call_index(17)] - #[pallet::weight(10_000)] + #[pallet::weight(T::WeightInfo::establish_storage_agreement())] pub fn establish_storage_agreement( origin: OriginFor, provider: T::AccountId, @@ -1430,7 +1430,7 @@ pub mod pallet { /// provider/capacity/stake checks and opens the replica agreement on /// an existing bucket. #[pallet::call_index(20)] - #[pallet::weight(10_000)] + #[pallet::weight(T::WeightInfo::establish_replica_agreement())] pub fn establish_replica_agreement( origin: OriginFor, bucket_id: BucketId, diff --git a/pallet/src/weights.rs b/pallet/src/weights.rs index ef3c08d3..82321253 100644 --- a/pallet/src/weights.rs +++ b/pallet/src/weights.rs @@ -19,7 +19,7 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-21, STEPS: `10`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `10`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -62,18 +62,13 @@ pub trait WeightInfo { fn add_stake() -> Weight; fn block_extensions() -> Weight; fn update_provider_multiaddr() -> Weight; - fn create_bucket() -> Weight; - fn create_bucket_with_storage() -> Weight; fn set_bucket_min_providers() -> Weight; fn freeze_bucket() -> Weight; fn set_bucket_member() -> Weight; fn remove_bucket_member() -> Weight; fn remove_slashed() -> Weight; - fn request_agreement() -> Weight; - fn request_primary_agreement() -> Weight; - fn accept_agreement() -> Weight; - fn reject_agreement() -> Weight; - fn withdraw_agreement_request() -> Weight; + fn establish_storage_agreement() -> Weight; + fn establish_replica_agreement() -> Weight; fn top_up_agreement() -> Weight; fn extend_agreement() -> Weight; fn end_agreement(a: u32, ) -> Weight; @@ -104,10 +99,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn register_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `183` + // Measured: `107` // Estimated: `3825` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 3825) + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(18_000_000, 3825) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -115,10 +110,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn deregister_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `345` + // Measured: `272` // Estimated: `3825` - // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(7_000_000, 3825) + // Minimum execution time: 7_000_000 picoseconds. + Weight::from_parts(8_000_000, 3825) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -130,10 +125,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn complete_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `45127` + // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 3_704_000_000 picoseconds. - Weight::from_parts(3_806_000_000, 2566553) + // Minimum execution time: 4_052_000_000 picoseconds. + Weight::from_parts(4_199_000_000, 2566553) .saturating_add(T::DbWeight::get().reads(1003_u64)) .saturating_add(T::DbWeight::get().writes(1002_u64)) } @@ -141,7 +136,7 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn cancel_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `328` + // Measured: `255` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 3825) @@ -152,7 +147,7 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_settings() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 3825) @@ -165,10 +160,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn add_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `427` + // Measured: `354` // Estimated: `3825` // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(16_000_000, 3825) + Weight::from_parts(17_000_000, 3825) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -178,10 +173,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn block_extensions() -> Weight { // Proof Size summary in bytes: - // Measured: `362` + // Measured: `399` // Estimated: `3825` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 3825) + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(11_000_000, 3825) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -189,57 +184,21 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_multiaddr() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 3825) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_bucket() -> Weight { - // Proof Size summary in bytes: - // Measured: `160` - // Estimated: `11515` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 11515) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(3_u64)) - } - /// Storage: `StorageProvider::Providers` (r:2 w:1) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn create_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `505` - // Estimated: `11515` - // Minimum execution time: 28_000_000 picoseconds. - Weight::from_parts(30_000_000, 11515) - .saturating_add(T::DbWeight::get().reads(5_u64)) - .saturating_add(T::DbWeight::get().writes(6_u64)) - } /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_bucket_min_providers() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` + // Measured: `358` + // Estimated: `3823` // Minimum execution time: 5_000_000 picoseconds. - Weight::from_parts(6_000_000, 3894) + Weight::from_parts(6_000_000, 3823) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -247,10 +206,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `581` - // Estimated: `4046` + // Measured: `510` + // Estimated: `3975` // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 4046) + Weight::from_parts(8_000_000, 3975) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -260,10 +219,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn set_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `507` + // Measured: `427` // Estimated: `11515` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 11515) + Weight::from_parts(11_000_000, 11515) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -273,7 +232,7 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn remove_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `602` + // Measured: `497` // Estimated: `11515` // Minimum execution time: 10_000_000 picoseconds. Weight::from_parts(11_000_000, 11515) @@ -290,89 +249,54 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `985` - // Estimated: `4450` - // Minimum execution time: 23_000_000 picoseconds. - Weight::from_parts(26_000_000, 4450) + // Measured: `951` + // Estimated: `4416` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(27_000_000, 4416) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } - /// Storage: `StorageProvider::Buckets` (r:1 w:0) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) + /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_agreement() -> Weight { + /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) + /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) + /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Buckets` (r:0 w:1) + /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) + fn establish_storage_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `612` - // Estimated: `4077` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(22_000_000, 4077) - .saturating_add(T::DbWeight::get().reads(4_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Measured: `354` + // Estimated: `11515` + // Minimum execution time: 48_000_000 picoseconds. + Weight::from_parts(52_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) } /// Storage: `StorageProvider::Buckets` (r:1 w:0) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_primary_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `774` - // Estimated: `4239` - // Minimum execution time: 21_000_000 picoseconds. - Weight::from_parts(23_000_000, 4239) - .saturating_add(T::DbWeight::get().reads(4_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } + /// Storage: `StorageProvider::StorageAgreements` (r:1 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:1 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn accept_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `833` - // Estimated: `4298` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(19_000_000, 4298) - .saturating_add(T::DbWeight::get().reads(3_u64)) - .saturating_add(T::DbWeight::get().writes(4_u64)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn reject_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(16_000_000, 3622) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn withdraw_agreement_request() -> Weight { + fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(17_000_000, 3622) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Measured: `737` + // Estimated: `4202` + // Minimum execution time: 47_000_000 picoseconds. + Weight::from_parts(53_000_000, 4202) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -382,10 +306,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn top_up_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `606` + // Measured: `643` // Estimated: `3825` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 3825) + // Minimum execution time: 21_000_000 picoseconds. + Weight::from_parts(23_000_000, 3825) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -397,10 +321,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn extend_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `709` + // Measured: `746` // Estimated: `6196` - // Minimum execution time: 49_000_000 picoseconds. - Weight::from_parts(53_000_000, 6196) + // Minimum execution time: 53_000_000 picoseconds. + Weight::from_parts(62_000_000, 6196) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -415,12 +339,12 @@ impl WeightInfo for SubstrateWeight { /// The range of component `a` is `[0, 1]`. fn end_agreement(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1088 + a * (103 ±0)` + // Measured: `1054 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` - // Minimum execution time: 48_000_000 picoseconds. - Weight::from_parts(51_229_669, 6196) - // Standard Error: 43_330 - .saturating_add(Weight::from_parts(23_380_250, 0).saturating_mul(a.into())) + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(56_690_740, 6196) + // Standard Error: 140_820 + .saturating_add(Weight::from_parts(24_681_481, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5_u64)) @@ -437,10 +361,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_expired_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `1088` + // Measured: `1054` // Estimated: `6196` - // Minimum execution time: 46_000_000 picoseconds. - Weight::from_parts(50_000_000, 6196) + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(56_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) } @@ -450,10 +374,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1768` + // Measured: `1697` // Estimated: `15165` - // Minimum execution time: 123_000_000 picoseconds. - Weight::from_parts(128_000_000, 15165) + // Minimum execution time: 128_000_000 picoseconds. + Weight::from_parts(143_000_000, 15165) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -463,10 +387,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn extend_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1822` + // Measured: `1751` // Estimated: `15165` - // Minimum execution time: 123_000_000 picoseconds. - Weight::from_parts(127_000_000, 15165) + // Minimum execution time: 127_000_000 picoseconds. + Weight::from_parts(142_000_000, 15165) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -478,10 +402,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::CheckpointPool` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) fn fund_checkpoint_pool() -> Weight { // Proof Size summary in bytes: - // Measured: `324` - // Estimated: `3789` + // Measured: `253` + // Estimated: `3718` // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(18_000_000, 3789) + Weight::from_parts(19_000_000, 3718) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -500,12 +424,12 @@ impl WeightInfo for SubstrateWeight { /// The range of component `s` is `[1, 5]`. fn provider_checkpoint(s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `564 + s * (258 ±0)` - // Estimated: `4029 + s * (2835 ±0)` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(19_431_221, 4029) - // Standard Error: 44_543 - .saturating_add(Weight::from_parts(23_612_253, 0).saturating_mul(s.into())) + // Measured: `493 + s * (258 ±0)` + // Estimated: `3958 + s * (2835 ±0)` + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(19_988_877, 3958) + // Standard Error: 70_760 + .saturating_add(Weight::from_parts(27_052_578, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4_u64)) @@ -517,10 +441,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::CheckpointConfigs` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) fn configure_checkpoint_window() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` + // Measured: `358` + // Estimated: `3823` // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(7_000_000, 3894) + Weight::from_parts(8_000_000, 3823) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -536,10 +460,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn report_missed_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `967` + // Measured: `896` // Estimated: `6196` - // Minimum execution time: 33_000_000 picoseconds. - Weight::from_parts(36_000_000, 6196) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(38_000_000, 6196) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -551,8 +475,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `364` // Estimated: `3593` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 3593) + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 3593) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -566,10 +490,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `893` - // Estimated: `4358` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 4358) + // Measured: `822` + // Estimated: `4287` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(23_000_000, 4287) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -585,10 +509,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `667` - // Estimated: `4132` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(46_000_000, 4132) + // Measured: `633` + // Estimated: `4098` + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(52_000_000, 4098) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -602,10 +526,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `755` - // Estimated: `4220` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(22_000_000, 4220) + // Measured: `792` + // Estimated: `4257` + // Minimum execution time: 21_000_000 picoseconds. + Weight::from_parts(25_000_000, 4257) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -619,10 +543,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `1131` + // Measured: `1060` // Estimated: `6196` - // Minimum execution time: 409_000_000 picoseconds. - Weight::from_parts(440_000_000, 6196) + // Minimum execution time: 420_000_000 picoseconds. + Weight::from_parts(469_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -636,10 +560,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn respond_to_challenge_deleted() -> Weight { // Proof Size summary in bytes: - // Measured: `1344` + // Measured: `1273` // Estimated: `6660` - // Minimum execution time: 56_000_000 picoseconds. - Weight::from_parts(60_000_000, 6660) + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(63_000_000, 6660) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -653,10 +577,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_superseded() -> Weight { // Proof Size summary in bytes: - // Measured: `1185` + // Measured: `1114` // Estimated: `6196` // Minimum execution time: 32_000_000 picoseconds. - Weight::from_parts(34_000_000, 6196) + Weight::from_parts(35_000_000, 6196) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } @@ -668,10 +592,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `1007` + // Measured: `973` // Estimated: `6196` - // Minimum execution time: 42_000_000 picoseconds. - Weight::from_parts(45_000_000, 6196) + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(49_000_000, 6196) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -681,10 +605,10 @@ impl WeightInfo for SubstrateWeight { /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn top_up_replica_sync_balance() -> Weight { // Proof Size summary in bytes: - // Measured: `473` + // Measured: `510` // Estimated: `3692` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 3692) + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(20_000_000, 3692) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -698,10 +622,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn register_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `183` + // Measured: `107` // Estimated: `3825` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 3825) + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(18_000_000, 3825) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -709,10 +633,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn deregister_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `345` + // Measured: `272` // Estimated: `3825` - // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(7_000_000, 3825) + // Minimum execution time: 7_000_000 picoseconds. + Weight::from_parts(8_000_000, 3825) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -724,10 +648,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn complete_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `45127` + // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 3_704_000_000 picoseconds. - Weight::from_parts(3_806_000_000, 2566553) + // Minimum execution time: 4_052_000_000 picoseconds. + Weight::from_parts(4_199_000_000, 2566553) .saturating_add(RocksDbWeight::get().reads(1003_u64)) .saturating_add(RocksDbWeight::get().writes(1002_u64)) } @@ -735,7 +659,7 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn cancel_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `328` + // Measured: `255` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 3825) @@ -746,7 +670,7 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_settings() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 3825) @@ -759,10 +683,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn add_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `427` + // Measured: `354` // Estimated: `3825` // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(16_000_000, 3825) + Weight::from_parts(17_000_000, 3825) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -772,10 +696,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn block_extensions() -> Weight { // Proof Size summary in bytes: - // Measured: `362` + // Measured: `399` // Estimated: `3825` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 3825) + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(11_000_000, 3825) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -783,57 +707,21 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_multiaddr() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 3825) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_bucket() -> Weight { - // Proof Size summary in bytes: - // Measured: `160` - // Estimated: `11515` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 11515) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(3_u64)) - } - /// Storage: `StorageProvider::Providers` (r:2 w:1) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn create_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `505` - // Estimated: `11515` - // Minimum execution time: 28_000_000 picoseconds. - Weight::from_parts(30_000_000, 11515) - .saturating_add(RocksDbWeight::get().reads(5_u64)) - .saturating_add(RocksDbWeight::get().writes(6_u64)) - } /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_bucket_min_providers() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` + // Measured: `358` + // Estimated: `3823` // Minimum execution time: 5_000_000 picoseconds. - Weight::from_parts(6_000_000, 3894) + Weight::from_parts(6_000_000, 3823) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -841,10 +729,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `581` - // Estimated: `4046` + // Measured: `510` + // Estimated: `3975` // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 4046) + Weight::from_parts(8_000_000, 3975) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -854,10 +742,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn set_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `507` + // Measured: `427` // Estimated: `11515` // Minimum execution time: 9_000_000 picoseconds. - Weight::from_parts(10_000_000, 11515) + Weight::from_parts(11_000_000, 11515) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -867,7 +755,7 @@ impl WeightInfo for () { /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn remove_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `602` + // Measured: `497` // Estimated: `11515` // Minimum execution time: 10_000_000 picoseconds. Weight::from_parts(11_000_000, 11515) @@ -884,89 +772,54 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `985` - // Estimated: `4450` - // Minimum execution time: 23_000_000 picoseconds. - Weight::from_parts(26_000_000, 4450) + // Measured: `951` + // Estimated: `4416` + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(27_000_000, 4416) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } - /// Storage: `StorageProvider::Buckets` (r:1 w:0) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) + /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_agreement() -> Weight { + /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) + /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) + /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Buckets` (r:0 w:1) + /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) + fn establish_storage_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `612` - // Estimated: `4077` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(22_000_000, 4077) - .saturating_add(RocksDbWeight::get().reads(4_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + // Measured: `354` + // Estimated: `11515` + // Minimum execution time: 48_000_000 picoseconds. + Weight::from_parts(52_000_000, 11515) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) } /// Storage: `StorageProvider::Buckets` (r:1 w:0) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_primary_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `774` - // Estimated: `4239` - // Minimum execution time: 21_000_000 picoseconds. - Weight::from_parts(23_000_000, 4239) - .saturating_add(RocksDbWeight::get().reads(4_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } + /// Storage: `StorageProvider::StorageAgreements` (r:1 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:1 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn accept_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `833` - // Estimated: `4298` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(19_000_000, 4298) - .saturating_add(RocksDbWeight::get().reads(3_u64)) - .saturating_add(RocksDbWeight::get().writes(4_u64)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn reject_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(16_000_000, 3622) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn withdraw_agreement_request() -> Weight { + fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(17_000_000, 3622) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + // Measured: `737` + // Estimated: `4202` + // Minimum execution time: 47_000_000 picoseconds. + Weight::from_parts(53_000_000, 4202) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -976,10 +829,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn top_up_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `606` + // Measured: `643` // Estimated: `3825` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 3825) + // Minimum execution time: 21_000_000 picoseconds. + Weight::from_parts(23_000_000, 3825) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -991,10 +844,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn extend_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `709` + // Measured: `746` // Estimated: `6196` - // Minimum execution time: 49_000_000 picoseconds. - Weight::from_parts(53_000_000, 6196) + // Minimum execution time: 53_000_000 picoseconds. + Weight::from_parts(62_000_000, 6196) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -1009,12 +862,12 @@ impl WeightInfo for () { /// The range of component `a` is `[0, 1]`. fn end_agreement(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1088 + a * (103 ±0)` + // Measured: `1054 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` - // Minimum execution time: 48_000_000 picoseconds. - Weight::from_parts(51_229_669, 6196) - // Standard Error: 43_330 - .saturating_add(Weight::from_parts(23_380_250, 0).saturating_mul(a.into())) + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(56_690_740, 6196) + // Standard Error: 140_820 + .saturating_add(Weight::from_parts(24_681_481, 0).saturating_mul(a.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(RocksDbWeight::get().writes(5_u64)) @@ -1031,10 +884,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_expired_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `1088` + // Measured: `1054` // Estimated: `6196` - // Minimum execution time: 46_000_000 picoseconds. - Weight::from_parts(50_000_000, 6196) + // Minimum execution time: 50_000_000 picoseconds. + Weight::from_parts(56_000_000, 6196) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } @@ -1044,10 +897,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1768` + // Measured: `1697` // Estimated: `15165` - // Minimum execution time: 123_000_000 picoseconds. - Weight::from_parts(128_000_000, 15165) + // Minimum execution time: 128_000_000 picoseconds. + Weight::from_parts(143_000_000, 15165) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -1057,10 +910,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn extend_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1822` + // Measured: `1751` // Estimated: `15165` - // Minimum execution time: 123_000_000 picoseconds. - Weight::from_parts(127_000_000, 15165) + // Minimum execution time: 127_000_000 picoseconds. + Weight::from_parts(142_000_000, 15165) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -1072,10 +925,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::CheckpointPool` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) fn fund_checkpoint_pool() -> Weight { // Proof Size summary in bytes: - // Measured: `324` - // Estimated: `3789` + // Measured: `253` + // Estimated: `3718` // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(18_000_000, 3789) + Weight::from_parts(19_000_000, 3718) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -1094,12 +947,12 @@ impl WeightInfo for () { /// The range of component `s` is `[1, 5]`. fn provider_checkpoint(s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `564 + s * (258 ±0)` - // Estimated: `4029 + s * (2835 ±0)` - // Minimum execution time: 40_000_000 picoseconds. - Weight::from_parts(19_431_221, 4029) - // Standard Error: 44_543 - .saturating_add(Weight::from_parts(23_612_253, 0).saturating_mul(s.into())) + // Measured: `493 + s * (258 ±0)` + // Estimated: `3958 + s * (2835 ±0)` + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(19_988_877, 3958) + // Standard Error: 70_760 + .saturating_add(Weight::from_parts(27_052_578, 0).saturating_mul(s.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(RocksDbWeight::get().writes(4_u64)) @@ -1111,10 +964,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::CheckpointConfigs` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) fn configure_checkpoint_window() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` + // Measured: `358` + // Estimated: `3823` // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(7_000_000, 3894) + Weight::from_parts(8_000_000, 3823) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -1130,10 +983,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn report_missed_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `967` + // Measured: `896` // Estimated: `6196` - // Minimum execution time: 33_000_000 picoseconds. - Weight::from_parts(36_000_000, 6196) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(38_000_000, 6196) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -1145,8 +998,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `364` // Estimated: `3593` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 3593) + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 3593) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -1160,10 +1013,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `893` - // Estimated: `4358` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 4358) + // Measured: `822` + // Estimated: `4287` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(23_000_000, 4287) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -1179,10 +1032,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `667` - // Estimated: `4132` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(46_000_000, 4132) + // Measured: `633` + // Estimated: `4098` + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(52_000_000, 4098) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -1196,10 +1049,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `755` - // Estimated: `4220` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(22_000_000, 4220) + // Measured: `792` + // Estimated: `4257` + // Minimum execution time: 21_000_000 picoseconds. + Weight::from_parts(25_000_000, 4257) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -1213,10 +1066,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `1131` + // Measured: `1060` // Estimated: `6196` - // Minimum execution time: 409_000_000 picoseconds. - Weight::from_parts(440_000_000, 6196) + // Minimum execution time: 420_000_000 picoseconds. + Weight::from_parts(469_000_000, 6196) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -1230,10 +1083,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn respond_to_challenge_deleted() -> Weight { // Proof Size summary in bytes: - // Measured: `1344` + // Measured: `1273` // Estimated: `6660` - // Minimum execution time: 56_000_000 picoseconds. - Weight::from_parts(60_000_000, 6660) + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(63_000_000, 6660) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -1247,10 +1100,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_superseded() -> Weight { // Proof Size summary in bytes: - // Measured: `1185` + // Measured: `1114` // Estimated: `6196` // Minimum execution time: 32_000_000 picoseconds. - Weight::from_parts(34_000_000, 6196) + Weight::from_parts(35_000_000, 6196) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } @@ -1262,10 +1115,10 @@ impl WeightInfo for () { /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `1007` + // Measured: `973` // Estimated: `6196` - // Minimum execution time: 42_000_000 picoseconds. - Weight::from_parts(45_000_000, 6196) + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(49_000_000, 6196) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -1275,10 +1128,10 @@ impl WeightInfo for () { /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn top_up_replica_sync_balance() -> Weight { // Proof Size summary in bytes: - // Measured: `473` + // Measured: `510` // Estimated: `3692` - // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 3692) + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(20_000_000, 3692) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } diff --git a/runtime/src/weights/pallet_storage_provider.rs b/runtime/src/weights/pallet_storage_provider.rs index a3b9d396..bf2decaf 100644 --- a/runtime/src/weights/pallet_storage_provider.rs +++ b/runtime/src/weights/pallet_storage_provider.rs @@ -17,7 +17,7 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -59,7 +59,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `183` // Estimated: `3825` // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_000_000, 0) + Weight::from_parts(15_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -86,8 +86,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `45127` // Estimated: `2566553` - // Minimum execution time: 3_620_000_000 picoseconds. - Weight::from_parts(3_785_000_000, 0) + // Minimum execution time: 3_490_000_000 picoseconds. + Weight::from_parts(3_661_000_000, 0) .saturating_add(Weight::from_parts(0, 2566553)) .saturating_add(T::DbWeight::get().reads(1003)) .saturating_add(T::DbWeight::get().writes(1002)) @@ -125,7 +125,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `427` // Estimated: `3825` // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(15_000_000, 0) + Weight::from_parts(14_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -167,7 +167,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `160` // Estimated: `11515` // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) + Weight::from_parts(8_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(3)) @@ -189,7 +189,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `505` // Estimated: `11515` // Minimum execution time: 25_000_000 picoseconds. - Weight::from_parts(28_000_000, 0) + Weight::from_parts(27_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(6)) @@ -259,7 +259,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `985` // Estimated: `4450` // Minimum execution time: 21_000_000 picoseconds. - Weight::from_parts(24_000_000, 0) + Weight::from_parts(23_000_000, 0) .saturating_add(Weight::from_parts(0, 4450)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) @@ -295,7 +295,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `774` // Estimated: `4239` // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 0) + Weight::from_parts(20_000_000, 0) .saturating_add(Weight::from_parts(0, 4239)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(2)) @@ -313,7 +313,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `833` // Estimated: `4298` // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(18_000_000, 0) + Weight::from_parts(17_000_000, 0) .saturating_add(Weight::from_parts(0, 4298)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(4)) @@ -392,10 +392,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `1088 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(46_523_979, 0) + Weight::from_parts(45_483_673, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 93_509 - .saturating_add(Weight::from_parts(20_951_020, 0).saturating_mul(a.into())) + // Standard Error: 147_900 + .saturating_add(Weight::from_parts(20_091_326, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5)) @@ -414,7 +414,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1088` // Estimated: `6196` - // Minimum execution time: 42_000_000 picoseconds. + // Minimum execution time: 41_000_000 picoseconds. Weight::from_parts(45_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) @@ -428,8 +428,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1768` // Estimated: `15165` - // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(129_000_000, 0) + // Minimum execution time: 121_000_000 picoseconds. + Weight::from_parts(125_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -443,7 +443,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `1822` // Estimated: `15165` // Minimum execution time: 121_000_000 picoseconds. - Weight::from_parts(129_000_000, 0) + Weight::from_parts(123_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -459,7 +459,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `324` // Estimated: `3789` // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 0) + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3789)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) @@ -482,10 +482,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `564 + s * (258 ±0)` // Estimated: `4029 + s * (2835 ±0)` // Minimum execution time: 38_000_000 picoseconds. - Weight::from_parts(17_845_151, 0) + Weight::from_parts(16_685_922, 0) .saturating_add(Weight::from_parts(0, 4029)) - // Standard Error: 49_996 - .saturating_add(Weight::from_parts(24_247_955, 0).saturating_mul(s.into())) + // Standard Error: 65_269 + .saturating_add(Weight::from_parts(23_495_268, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4)) @@ -608,7 +608,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `1131` // Estimated: `6196` // Minimum execution time: 407_000_000 picoseconds. - Weight::from_parts(436_000_000, 0) + Weight::from_parts(434_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -626,7 +626,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `1344` // Estimated: `6660` // Minimum execution time: 53_000_000 picoseconds. - Weight::from_parts(58_000_000, 0) + Weight::from_parts(57_000_000, 0) .saturating_add(Weight::from_parts(0, 6660)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -643,8 +643,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1185` // Estimated: `6196` - // Minimum execution time: 29_000_000 picoseconds. - Weight::from_parts(32_000_000, 0) + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(31_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -660,7 +660,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `1007` // Estimated: `6196` // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(41_000_000, 0) + Weight::from_parts(40_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) @@ -674,7 +674,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `473` // Estimated: `3692` // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_000_000, 0) + Weight::from_parts(15_000_000, 0) .saturating_add(Weight::from_parts(0, 3692)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs b/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs index 8e0ebac9..58de557a 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs @@ -17,7 +17,7 @@ //! Autogenerated weights for `pallet_drive_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -52,71 +52,71 @@ pub struct WeightInfo(PhantomData); impl pallet_drive_registry::WeightInfo for WeightInfo { /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:5 w:5) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::NextDriveId` (r:1 w:1) /// Proof: `DriveRegistry::NextDriveId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::Drives` (r:0 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `2351` - // Estimated: `17970` - // Minimum execution time: 111_000_000 picoseconds. - Weight::from_parts(121_000_000, 0) - .saturating_add(Weight::from_parts(0, 17970)) - .saturating_add(T::DbWeight::get().reads(16)) - .saturating_add(T::DbWeight::get().writes(13)) + // Measured: `1301` + // Estimated: `11515` + // Minimum execution time: 54_000_000 picoseconds. + Weight::from_parts(58_000_000, 0) + .saturating_add(Weight::from_parts(0, 11515)) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(11)) } /// Storage: `DriveRegistry::Drives` (r:1 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:6 w:5) + /// Storage: `StorageProvider::StorageAgreements` (r:2 w:1) /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:6 w:6) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:5 w:5) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:100 w:100) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:0) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn delete_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `14780` + // Measured: `12052` // Estimated: `1053490` - // Minimum execution time: 451_000_000 picoseconds. - Weight::from_parts(484_000_000, 0) + // Minimum execution time: 301_000_000 picoseconds. + Weight::from_parts(326_000_000, 0) .saturating_add(Weight::from_parts(0, 1053490)) - .saturating_add(T::DbWeight::get().reads(121)) - .saturating_add(T::DbWeight::get().writes(120)) + .saturating_add(T::DbWeight::get().reads(108)) + .saturating_add(T::DbWeight::get().writes(108)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn share_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4813` + // Measured: `4757` // Estimated: `11515` // Minimum execution time: 18_000_000 picoseconds. Weight::from_parts(20_000_000, 0) @@ -125,14 +125,14 @@ impl pallet_drive_registry::WeightInfo for WeightInfo Weight { // Proof Size summary in bytes: - // Measured: `4840` + // Measured: `4784` // Estimated: `11515` // Minimum execution time: 18_000_000 picoseconds. Weight::from_parts(20_000_000, 0) diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_s3_registry.rs b/runtimes/web3-storage-paseo/src/weights/pallet_s3_registry.rs index 60214008..84645bc6 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_s3_registry.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_s3_registry.rs @@ -17,7 +17,7 @@ //! Autogenerated weights for `pallet_s3_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-11, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -54,6 +54,12 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) /// Storage: `S3Registry::UserBuckets` (r:1 w:1) /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) @@ -64,15 +70,17 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_s3_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `1107` + // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 0) + // Minimum execution time: 55_000_000 picoseconds. + Weight::from_parts(59_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(7)) + .saturating_add(T::DbWeight::get().reads(8)) + .saturating_add(T::DbWeight::get().writes(11)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -84,7 +92,7 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1169` // Estimated: `4315` - // Minimum execution time: 9_000_000 picoseconds. + // Minimum execution time: 10_000_000 picoseconds. Weight::from_parts(11_000_000, 0) .saturating_add(Weight::from_parts(0, 4315)) .saturating_add(T::DbWeight::get().reads(2)) @@ -99,7 +107,7 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Measured: `8038` // Estimated: `11176` // Minimum execution time: 26_000_000 picoseconds. - Weight::from_parts(29_000_000, 0) + Weight::from_parts(28_000_000, 0) .saturating_add(Weight::from_parts(0, 11176)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -132,34 +140,4 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(2)) } - /// Storage: `S3Registry::BucketNameToId` (r:1 w:1) - /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::UserBuckets` (r:1 w:1) - /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::NextS3BucketId` (r:1 w:1) - /// Proof: `S3Registry::NextS3BucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::S3Buckets` (r:0 w:1) - /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_s3_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `2351` - // Estimated: `17970` - // Minimum execution time: 51_000_000 picoseconds. - Weight::from_parts(56_000_000, 0) - .saturating_add(Weight::from_parts(0, 17970)) - .saturating_add(T::DbWeight::get().reads(13)) - .saturating_add(T::DbWeight::get().writes(9)) - } } diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs b/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs index 16c6e1cb..b9f1f976 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs @@ -17,7 +17,7 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-26, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -56,10 +56,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn register_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `183` + // Measured: `107` // Estimated: `3825` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_000_000, 0) + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(15_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -68,7 +68,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn deregister_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `345` + // Measured: `272` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 0) @@ -84,10 +84,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn complete_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `45127` + // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 3_699_000_000 picoseconds. - Weight::from_parts(3_779_000_000, 0) + // Minimum execution time: 3_686_000_000 picoseconds. + Weight::from_parts(3_821_000_000, 0) .saturating_add(Weight::from_parts(0, 2566553)) .saturating_add(T::DbWeight::get().reads(1003)) .saturating_add(T::DbWeight::get().writes(1002)) @@ -96,7 +96,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn cancel_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `328` + // Measured: `255` // Estimated: `3825` // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(6_000_000, 0) @@ -108,7 +108,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_settings() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(6_000_000, 0) @@ -122,7 +122,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn add_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `427` + // Measured: `354` // Estimated: `3825` // Minimum execution time: 13_000_000 picoseconds. Weight::from_parts(15_000_000, 0) @@ -136,9 +136,9 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn block_extensions() -> Weight { // Proof Size summary in bytes: - // Measured: `362` + // Measured: `399` // Estimated: `3825` - // Minimum execution time: 7_000_000 picoseconds. + // Minimum execution time: 8_000_000 picoseconds. Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) @@ -148,7 +148,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_multiaddr() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(6_000_000, 0) @@ -156,53 +156,15 @@ impl pallet_storage_provider::WeightInfo for WeightInfo .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_bucket() -> Weight { - // Proof Size summary in bytes: - // Measured: `160` - // Estimated: `11515` - // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: `StorageProvider::Providers` (r:2 w:1) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn create_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `505` - // Estimated: `11515` - // Minimum execution time: 25_000_000 picoseconds. - Weight::from_parts(28_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(6)) - } /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_bucket_min_providers() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` + // Measured: `358` + // Estimated: `3823` // Minimum execution time: 4_000_000 picoseconds. Weight::from_parts(5_000_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -210,11 +172,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `581` - // Estimated: `4046` + // Measured: `510` + // Estimated: `3975` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 0) - .saturating_add(Weight::from_parts(0, 4046)) + .saturating_add(Weight::from_parts(0, 3975)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -224,10 +186,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn set_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `507` + // Measured: `427` // Estimated: `11515` // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(10_000_000, 0) + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -238,7 +200,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn remove_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `602` + // Measured: `497` // Estimated: `11515` // Minimum execution time: 9_000_000 picoseconds. Weight::from_parts(10_000_000, 0) @@ -256,95 +218,57 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `985` - // Estimated: `4450` + // Measured: `951` + // Estimated: `4416` // Minimum execution time: 22_000_000 picoseconds. Weight::from_parts(24_000_000, 0) - .saturating_add(Weight::from_parts(0, 4450)) + .saturating_add(Weight::from_parts(0, 4416)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } - /// Storage: `StorageProvider::Buckets` (r:1 w:0) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) + /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_agreement() -> Weight { + /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) + /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) + /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Buckets` (r:0 w:1) + /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) + fn establish_storage_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `612` - // Estimated: `4077` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) - .saturating_add(Weight::from_parts(0, 4077)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `354` + // Estimated: `11515` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(48_000_000, 0) + .saturating_add(Weight::from_parts(0, 11515)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `StorageProvider::Buckets` (r:1 w:0) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_primary_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `774` - // Estimated: `4239` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 0) - .saturating_add(Weight::from_parts(0, 4239)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) - } + /// Storage: `StorageProvider::StorageAgreements` (r:1 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:1 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn accept_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `833` - // Estimated: `4298` - // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(18_000_000, 0) - .saturating_add(Weight::from_parts(0, 4298)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn reject_agreement() -> Weight { + fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(15_000_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn withdraw_agreement_request() -> Weight { - // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_000_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `737` + // Estimated: `4202` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(47_000_000, 0) + .saturating_add(Weight::from_parts(0, 4202)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -354,10 +278,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn top_up_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `606` + // Measured: `643` // Estimated: `3825` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 0) + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) @@ -370,10 +294,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn extend_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `709` + // Measured: `746` // Estimated: `6196` - // Minimum execution time: 44_000_000 picoseconds. - Weight::from_parts(48_000_000, 0) + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) @@ -389,13 +313,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `a` is `[0, 1]`. fn end_agreement(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1088 + a * (103 ±0)` + // Measured: `1054 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(46_650_510, 0) + Weight::from_parts(46_987_755, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 76_217 - .saturating_add(Weight::from_parts(20_724_489, 0).saturating_mul(a.into())) + // Standard Error: 138_728 + .saturating_add(Weight::from_parts(22_012_244, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5)) @@ -412,9 +336,9 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_expired_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `1088` + // Measured: `1054` // Estimated: `6196` - // Minimum execution time: 41_000_000 picoseconds. + // Minimum execution time: 42_000_000 picoseconds. Weight::from_parts(46_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) @@ -426,10 +350,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1768` + // Measured: `1697` // Estimated: `15165` - // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(131_000_000, 0) + // Minimum execution time: 121_000_000 picoseconds. + Weight::from_parts(130_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -440,10 +364,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn extend_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1822` + // Measured: `1751` // Estimated: `15165` // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(130_000_000, 0) + Weight::from_parts(132_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -456,11 +380,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointPool` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) fn fund_checkpoint_pool() -> Weight { // Proof Size summary in bytes: - // Measured: `324` - // Estimated: `3789` + // Measured: `253` + // Estimated: `3718` // Minimum execution time: 15_000_000 picoseconds. - Weight::from_parts(17_000_000, 0) - .saturating_add(Weight::from_parts(0, 3789)) + Weight::from_parts(16_000_000, 0) + .saturating_add(Weight::from_parts(0, 3718)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -479,13 +403,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `s` is `[1, 5]`. fn provider_checkpoint(s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `564 + s * (258 ±0)` - // Estimated: `4029 + s * (2835 ±0)` - // Minimum execution time: 39_000_000 picoseconds. - Weight::from_parts(17_890_712, 0) - .saturating_add(Weight::from_parts(0, 4029)) - // Standard Error: 17_474 - .saturating_add(Weight::from_parts(24_134_637, 0).saturating_mul(s.into())) + // Measured: `493 + s * (258 ±0)` + // Estimated: `3958 + s * (2835 ±0)` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(16_923_714, 0) + .saturating_add(Weight::from_parts(0, 3958)) + // Standard Error: 31_634 + .saturating_add(Weight::from_parts(24_196_144, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4)) @@ -497,11 +421,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointConfigs` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) fn configure_checkpoint_window() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` - // Minimum execution time: 6_000_000 picoseconds. + // Measured: `358` + // Estimated: `3823` + // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(7_000_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -517,7 +441,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn report_missed_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `967` + // Measured: `896` // Estimated: `6196` // Minimum execution time: 30_000_000 picoseconds. Weight::from_parts(33_000_000, 0) @@ -549,11 +473,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `893` - // Estimated: `4358` + // Measured: `822` + // Estimated: `4287` // Minimum execution time: 17_000_000 picoseconds. Weight::from_parts(19_000_000, 0) - .saturating_add(Weight::from_parts(0, 4358)) + .saturating_add(Weight::from_parts(0, 4287)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -569,11 +493,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `667` - // Estimated: `4132` + // Measured: `633` + // Estimated: `4098` // Minimum execution time: 41_000_000 picoseconds. Weight::from_parts(44_000_000, 0) - .saturating_add(Weight::from_parts(0, 4132)) + .saturating_add(Weight::from_parts(0, 4098)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -587,11 +511,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `755` - // Estimated: `4220` + // Measured: `792` + // Estimated: `4257` // Minimum execution time: 19_000_000 picoseconds. Weight::from_parts(21_000_000, 0) - .saturating_add(Weight::from_parts(0, 4220)) + .saturating_add(Weight::from_parts(0, 4257)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -605,10 +529,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `1131` + // Measured: `1060` // Estimated: `6196` - // Minimum execution time: 407_000_000 picoseconds. - Weight::from_parts(435_000_000, 0) + // Minimum execution time: 406_000_000 picoseconds. + Weight::from_parts(433_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -623,10 +547,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn respond_to_challenge_deleted() -> Weight { // Proof Size summary in bytes: - // Measured: `1344` + // Measured: `1273` // Estimated: `6660` - // Minimum execution time: 54_000_000 picoseconds. - Weight::from_parts(58_000_000, 0) + // Minimum execution time: 53_000_000 picoseconds. + Weight::from_parts(57_000_000, 0) .saturating_add(Weight::from_parts(0, 6660)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -641,10 +565,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_superseded() -> Weight { // Proof Size summary in bytes: - // Measured: `1185` + // Measured: `1114` // Estimated: `6196` - // Minimum execution time: 29_000_000 picoseconds. - Weight::from_parts(32_000_000, 0) + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(31_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -657,10 +581,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `1007` + // Measured: `973` // Estimated: `6196` - // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(41_000_000, 0) + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(42_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) @@ -671,9 +595,9 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn top_up_replica_sync_balance() -> Weight { // Proof Size summary in bytes: - // Measured: `473` + // Measured: `510` // Estimated: `3692` - // Minimum execution time: 14_000_000 picoseconds. + // Minimum execution time: 15_000_000 picoseconds. Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3692)) .saturating_add(T::DbWeight::get().reads(2)) diff --git a/runtimes/web3-storage-paseo/tests/tests.rs b/runtimes/web3-storage-paseo/tests/tests.rs index 12a3fe5a..f6994f04 100644 --- a/runtimes/web3-storage-paseo/tests/tests.rs +++ b/runtimes/web3-storage-paseo/tests/tests.rs @@ -17,6 +17,7 @@ use storage_paseo_runtime::{ RuntimeOrigin, S3Registry, SessionKeys, StorageProvider, System, TxExtension, UncheckedExtrinsic, WeightToFee, }; +use storage_primitives::AgreementTerms; use xcm::latest::prelude::*; use xcm_runtime_apis::conversions::LocationToAccountHelper; @@ -134,6 +135,36 @@ fn register_provider_for(account: Sr25519Keyring, stake: Balance) { )); } +/// Build primary [`AgreementTerms`] for `owner` with the standard test shape. +fn primary_terms( + owner: AccountId, + max_bytes: u64, + duration: u32, + nonce: u64, +) -> pallet_storage_provider::AgreementTermsOf { + AgreementTerms { + owner, + max_bytes, + duration: duration.into(), + price_per_byte: 0, + valid_until: 1_000_000_000, + nonce, + replica_params: None, + } +} + +/// Sign SCALE-encoded agreement terms with the provider keyring's sr25519 key. +/// +/// `register_provider_for` stores the keyring's raw public key in the pallet, +/// so signatures produced here verify against that stored key. +fn sign_primary_terms( + provider: Sr25519Keyring, + terms: &pallet_storage_provider::AgreementTermsOf, +) -> sp_runtime::MultiSignature { + let hash = sp_io::hashing::blake2_256(&terms.encode()); + sp_runtime::MultiSignature::Sr25519(provider.pair().sign(&hash)) +} + fn new_test_ext() -> sp_io::TestExternalities { sp_io::TestExternalities::new(RuntimeGenesisConfig::default().build_storage().unwrap()) } @@ -541,36 +572,6 @@ fn should_update_provider_settings() { }); } -#[test] -fn should_create_bucket() { - new_test_ext().execute_with(|| { - let account = Sr25519Keyring::Alice; - let who: AccountId = account.to_account_id(); - // Fund the caller so any tx fees can be paid. - let _ = Balances::deposit_creating(&who, default_stake()); - - let bucket_id_before = pallet_storage_provider::NextBucketId::::get(); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(account.pair()), - RuntimeCall::StorageProvider(StorageProviderCall::::create_bucket { - min_providers: 1, - }), - )); - - let bucket = pallet_storage_provider::Buckets::::get(bucket_id_before) - .expect("bucket must be stored"); - assert_eq!(bucket.min_providers, 1); - assert_eq!( - pallet_storage_provider::NextBucketId::::get(), - bucket_id_before + 1 - ); - - let owned = pallet_storage_provider::MemberBuckets::::get(&who); - assert!(owned.contains(&bucket_id_before)); - }); -} - // =============================== // Tests for the storage provider pallet via XCM. // =============================== @@ -776,126 +777,71 @@ fn should_fail_xcm_unpaid_execution_from_unauthorized_origin() { // Tests for the Drive registry pallet. // =============================== -/// Register `account` as a provider and configure it to accept primary agreements with -/// unlimited capacity (`max_capacity = 0`), so `create_drive` can find it. +/// Register `account` as a provider and configure it to accept primary +/// agreements with unlimited capacity and a wide duration window, so the +/// E2E flows can submit signed terms with arbitrary durations. fn register_accepting_provider_for(account: Sr25519Keyring, stake: Balance) { register_provider_for(account, stake); assert_ok_ok(construct_and_apply_extrinsic( Some(account.pair()), RuntimeCall::StorageProvider(StorageProviderCall::::update_provider_settings { settings: pallet_storage_provider::ProviderSettings { + min_duration: 1, + max_duration: 1_000_000, + price_per_byte: 0, accepting_primary: true, - max_capacity: 0, - ..Default::default() + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 0, // Unlimited }, }), )); } +/// End-to-end drive lifecycle: provider setup → signed-terms create_drive → +/// share with a member → unshare → delete drive. Walks the complete +/// happy-path surface exposed by `pallet-drive-registry` so a single test +/// can spot regressions across the whole flow. #[test] -fn should_create_drive() { +fn drive_lifecycle_e2e() { new_test_ext().execute_with(|| { - let provider = Sr25519Keyring::Bob; - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - let stake = default_stake(); + advance_block(); - register_accepting_provider_for(provider, stake); + let provider = Sr25519Keyring::Bob; + let owner = Sr25519Keyring::Alice; + let owner_id: AccountId = owner.to_account_id(); + let member_id: AccountId = Sr25519Keyring::Charlie.to_account_id(); - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); + // 1. Provider registers + accepts primary agreements. + register_accepting_provider_for(provider, default_stake()); + let _ = Balances::deposit_creating(&owner_id, 10 * UNIT); - let drive_id_before = pallet_drive_registry::NextDriveId::::get(); + // 2. Owner redeems provider-signed terms to create the drive. + let drive_id = pallet_drive_registry::NextDriveId::::get(); + let terms = primary_terms(owner_id.clone(), 1_000_000, 500, 1); + let sig = sign_primary_terms(provider, &terms); assert_ok_ok(construct_and_apply_extrinsic( - Some(user.pair()), + Some(owner.pair()), RuntimeCall::DriveRegistry(DriveRegistryCall::::create_drive { name: Some(b"My Drive".to_vec()), - max_capacity: 1_000_000, - storage_period: 500, - payment: UNIT, - min_providers: Some(1), + provider: provider.to_account_id(), + terms, + sig, }), )); - let drive = pallet_drive_registry::Drives::::get(drive_id_before) + let drive = pallet_drive_registry::Drives::::get(drive_id) .expect("drive must be stored"); - assert_eq!(drive.owner, user_id); + assert_eq!(drive.owner, owner_id); assert_eq!(drive.max_capacity, 1_000_000); - - let user_drives = pallet_drive_registry::UserDrives::::get(&user_id); - assert!(user_drives.contains(&drive_id_before)); - + assert!(pallet_drive_registry::UserDrives::::get(&owner_id).contains(&drive_id)); assert_eq!( pallet_drive_registry::NextDriveId::::get(), - drive_id_before + 1 + drive_id + 1 ); - }); -} - -#[test] -fn should_delete_drive() { - new_test_ext().execute_with(|| { - let provider = Sr25519Keyring::Bob; - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - let stake = default_stake(); - - register_accepting_provider_for(provider, stake); - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - - let drive_id = pallet_drive_registry::NextDriveId::::get(); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(user.pair()), - RuntimeCall::DriveRegistry(DriveRegistryCall::::create_drive { - name: None, - max_capacity: 1_000_000, - storage_period: 500, - payment: UNIT, - min_providers: Some(1), - }), - )); - - assert!(pallet_drive_registry::Drives::::get(drive_id).is_some()); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(user.pair()), - RuntimeCall::DriveRegistry(DriveRegistryCall::::delete_drive { drive_id }), - )); - - assert!(pallet_drive_registry::Drives::::get(drive_id).is_none()); - let user_drives = pallet_drive_registry::UserDrives::::get(&user_id); - assert!(!user_drives.contains(&drive_id)); - }); -} - -#[test] -fn should_share_and_unshare_drive() { - new_test_ext().execute_with(|| { - advance_block(); - - let provider = Sr25519Keyring::Charlie; - let owner = Sr25519Keyring::Alice; - let owner_id: AccountId = owner.to_account_id(); - let member_id: AccountId = Sr25519Keyring::Bob.to_account_id(); - let stake = default_stake(); - - register_accepting_provider_for(provider, stake); - let _ = Balances::deposit_creating(&owner_id, 10 * UNIT); - - let drive_id = pallet_drive_registry::NextDriveId::::get(); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(owner.pair()), - RuntimeCall::DriveRegistry(DriveRegistryCall::::create_drive { - name: None, - max_capacity: 1_000_000, - storage_period: 500, - payment: UNIT, - min_providers: Some(1), - }), - )); + // 3. Share with Charlie as Reader, then revoke. assert_ok_ok(construct_and_apply_extrinsic( Some(owner.pair()), RuntimeCall::DriveRegistry(DriveRegistryCall::::share_drive { @@ -904,7 +850,6 @@ fn should_share_and_unshare_drive() { role: storage_primitives::Role::Reader, }), )); - System::assert_has_event(RuntimeEvent::DriveRegistry( pallet_drive_registry::Event::DriveShared { drive_id, @@ -920,13 +865,20 @@ fn should_share_and_unshare_drive() { member: member_id.clone(), }), )); - System::assert_has_event(RuntimeEvent::DriveRegistry( pallet_drive_registry::Event::DriveUnshared { drive_id, member: member_id, }, )); + + // 4. Delete the drive. + assert_ok_ok(construct_and_apply_extrinsic( + Some(owner.pair()), + RuntimeCall::DriveRegistry(DriveRegistryCall::::delete_drive { drive_id }), + )); + assert!(pallet_drive_registry::Drives::::get(drive_id).is_none()); + assert!(!pallet_drive_registry::UserDrives::::get(&owner_id).contains(&drive_id)); }); } @@ -934,116 +886,92 @@ fn should_share_and_unshare_drive() { // Tests for the Drive registry pallet via XCM. // =============================== +/// End-to-end drive lifecycle dispatched via XCM from a sibling parachain: +/// the call's `Signed` origin is the *derived sovereign* of the +/// `(Parachain(N), AccountId32(Alice))` location, so the provider must sign +/// terms with `terms.owner = derived` (not Alice's keyring account). #[test] -fn should_create_drive_via_xcm() { - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - - let create_drive_call = - RuntimeCall::DriveRegistry(DriveRegistryCall::::create_drive { - name: Some(b"My XCM Drive".to_vec()), - max_capacity: 1_000_000, - storage_period: 500, - payment: UNIT, - min_providers: Some(1), - }); +fn drive_lifecycle_via_xcm_e2e() { + let alice_on_para = alice_on_sibling_parachain(4_000); + let provider = Sr25519Keyring::Bob; + let member_id: AccountId = Sr25519Keyring::Charlie.to_account_id(); xcm_test_ext().execute_with(|| { - // Bob is the storage provider; Alice is the drive owner submitting via XCM. - register_accepting_provider_for(Sr25519Keyring::Bob, default_stake()); - - // Alice needs balance for both the payment token and XCM execution fees. - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - - let drive_id_before = pallet_drive_registry::NextDriveId::::get(); + // The dispatch origin is the sovereign `AccountId` derived from + // Alice-on-para; that's who the drive will belong to. + let derived: AccountId = + LocationToAccountHelper::::convert_location( + alice_on_para.clone().into(), + ) + .expect("Alice-on-para must convert to an account"); - // Alice's local account location. With `OriginKind::SovereignAccount`, - // `AccountId32Aliases` maps this directly to Alice's `AccountId` — - // so the drive is owned by Alice herself. - let alice_location = Location::new( - 0, - [Junction::AccountId32 { - network: None, - id: user.to_raw_public(), - }], - ); + // 1. Provider setup; fund the derived account for fees. + register_accepting_provider_for(provider, default_stake()); + let _ = Balances::deposit_creating(&derived, 10 * UNIT); - // Pay XCM execution fees from Alice's balance. + let drive_id = pallet_drive_registry::NextDriveId::::get(); let fee: Asset = (Location::parent(), UNIT).into(); + // 2. Provider signs terms for the derived sovereign + XCM dispatches. + let terms = primary_terms(derived.clone(), 1_000_000, 500, 1); + let sig = sign_primary_terms(provider, &terms); + let create_drive_call = + RuntimeCall::DriveRegistry(DriveRegistryCall::::create_drive { + name: Some(b"Sibling Drive".to_vec()), + provider: provider.to_account_id(), + terms, + sig, + }); + assert_ok!( RuntimeHelper::::execute_as_origin( - (alice_location, OriginKind::SovereignAccount), + (alice_on_para.clone(), OriginKind::SovereignAccount), create_drive_call, - Some(fee), + Some(fee.clone()), ) .ensure_complete() ); - System::assert_has_event(RuntimeEvent::DriveRegistry( - pallet_drive_registry::Event::DriveCreated { - drive_id: drive_id_before, - owner: user_id.clone(), - bucket_id: pallet_drive_registry::Drives::::get(drive_id_before) - .expect("drive must be stored") - .bucket_id, - }, - )); - - let drive = pallet_drive_registry::Drives::::get(drive_id_before) + let drive = pallet_drive_registry::Drives::::get(drive_id) .expect("drive must be stored"); - assert_eq!(drive.owner, user_id); + assert_eq!(drive.owner, derived); assert_eq!(drive.max_capacity, 1_000_000); - }); -} + assert!(pallet_drive_registry::UserDrives::::get(&derived).contains(&drive_id)); -#[test] -fn should_create_drive_via_xcm_from_sibling_parachain() { - // Use a distinct para id so the derived sovereign is different from other tests. - let alice_on_para = alice_on_sibling_parachain(4_000); - - let create_drive_call = - RuntimeCall::DriveRegistry(DriveRegistryCall::::create_drive { - name: Some(b"Sibling Drive".to_vec()), - max_capacity: 1_000_000, - storage_period: 500, - payment: UNIT, - min_providers: Some(1), + // 3. Share with Charlie via XCM from the same origin. + let share_call = RuntimeCall::DriveRegistry(DriveRegistryCall::::share_drive { + drive_id, + member: member_id.clone(), + role: storage_primitives::Role::Reader, }); - - xcm_test_ext().execute_with(|| { - // The dispatch origin is the sovereign `AccountId` derived from Alice-on-para. - let derived: AccountId = - LocationToAccountHelper::::convert_location( - alice_on_para.clone().into(), + assert_ok!( + RuntimeHelper::::execute_as_origin( + (alice_on_para.clone(), OriginKind::SovereignAccount), + share_call, + Some(fee.clone()), ) - .expect("Alice-on-para must convert to an account"); - - // Bob is the storage provider; the derived sovereign account owns the drive. - register_accepting_provider_for(Sr25519Keyring::Bob, default_stake()); - - // Fund the derived account for the drive payment and XCM execution fees. - let _ = Balances::deposit_creating(&derived, 10 * UNIT); - - let drive_id_before = pallet_drive_registry::NextDriveId::::get(); - let fee: Asset = (Location::parent(), UNIT).into(); + .ensure_complete() + ); + System::assert_has_event(RuntimeEvent::DriveRegistry( + pallet_drive_registry::Event::DriveShared { + drive_id, + member: member_id.clone(), + role: storage_primitives::Role::Reader, + }, + )); + // 4. Delete the drive via XCM. + let delete_call = + RuntimeCall::DriveRegistry(DriveRegistryCall::::delete_drive { drive_id }); assert_ok!( RuntimeHelper::::execute_as_origin( (alice_on_para, OriginKind::SovereignAccount), - create_drive_call, + delete_call, Some(fee), ) .ensure_complete() ); - - let drive = pallet_drive_registry::Drives::::get(drive_id_before) - .expect("drive must be stored"); - assert_eq!(drive.owner, derived); - assert_eq!(drive.max_capacity, 1_000_000); - - let user_drives = pallet_drive_registry::UserDrives::::get(&derived); - assert!(user_drives.contains(&drive_id_before)); + assert!(pallet_drive_registry::Drives::::get(drive_id).is_none()); }); } @@ -1051,91 +979,54 @@ fn should_create_drive_via_xcm_from_sibling_parachain() { // Tests for the S3 registry pallet. // =============================== +/// End-to-end S3 bucket lifecycle: provider setup → signed-terms +/// `create_s3_bucket` → put object metadata → delete object → delete bucket. +/// Combines the full happy-path object-store surface so a regression in any +/// step is caught by a single test. #[test] -fn should_create_s3_bucket() { +fn s3_bucket_lifecycle_e2e() { new_test_ext().execute_with(|| { + advance_block(); + + let provider = Sr25519Keyring::Bob; let user = Sr25519Keyring::Alice; let user_id: AccountId = user.to_account_id(); + // 1. Provider setup. + register_accepting_provider_for(provider, default_stake()); let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - let s3_bucket_id_before = pallet_s3_registry::NextS3BucketId::::get(); - + // 2. Owner redeems provider-signed terms to create the S3 bucket. + let s3_bucket_id = pallet_s3_registry::NextS3BucketId::::get(); + let terms = primary_terms(user_id.clone(), 1_000_000, 500, 1); + let sig = sign_primary_terms(provider, &terms); assert_ok_ok(construct_and_apply_extrinsic( Some(user.pair()), RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket { - name: b"my-test-bucket".to_vec(), - min_providers: 1, + name: b"my-bucket".to_vec(), + provider: provider.to_account_id(), + terms, + sig, }), )); - let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id_before) + let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id) .expect("bucket must be stored"); assert_eq!(bucket.owner, user_id); - assert_eq!(bucket.name.as_slice(), b"my-test-bucket"); + assert_eq!(bucket.name.as_slice(), b"my-bucket"); assert_eq!(bucket.object_count, 0); - - let user_buckets = pallet_s3_registry::UserBuckets::::get(&user_id); - assert!(user_buckets.contains(&s3_bucket_id_before)); - - assert_eq!( - pallet_s3_registry::NextS3BucketId::::get(), - s3_bucket_id_before + 1 - ); - }); -} - -#[test] -fn should_delete_s3_bucket() { - new_test_ext().execute_with(|| { - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - - let s3_bucket_id = pallet_s3_registry::NextS3BucketId::::get(); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(user.pair()), - RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket { - name: b"delete-me".to_vec(), - min_providers: 1, - }), - )); - - assert!(pallet_s3_registry::S3Buckets::::get(s3_bucket_id).is_some()); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(user.pair()), - RuntimeCall::S3Registry(S3RegistryCall::::delete_s3_bucket { s3_bucket_id }), - )); - - assert!(pallet_s3_registry::S3Buckets::::get(s3_bucket_id).is_none()); - let user_buckets = pallet_s3_registry::UserBuckets::::get(&user_id); - assert!(!user_buckets.contains(&s3_bucket_id)); - }); -} - -#[test] -fn should_put_and_delete_object_metadata() { - new_test_ext().execute_with(|| { - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - - let s3_bucket_id = pallet_s3_registry::NextS3BucketId::::get(); - - assert_ok_ok(construct_and_apply_extrinsic( - Some(user.pair()), - RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket { - name: b"object-bucket".to_vec(), - min_providers: 1, - }), + assert!(pallet_s3_registry::UserBuckets::::get(&user_id).contains(&s3_bucket_id)); + System::assert_has_event(RuntimeEvent::S3Registry( + pallet_s3_registry::Event::S3BucketCreated { + s3_bucket_id, + name: b"my-bucket".to_vec(), + layer0_bucket_id: bucket.layer0_bucket_id, + owner: user_id.clone(), + }, )); + // 3. Put an object, verify metadata + bucket stats updated. let cid = sp_core::H256::repeat_byte(0xAB); - assert_ok_ok(construct_and_apply_extrinsic( Some(user.pair()), RuntimeCall::S3Registry(S3RegistryCall::::put_object_metadata { @@ -1147,16 +1038,15 @@ fn should_put_and_delete_object_metadata() { user_metadata: vec![], }), )); - let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id).unwrap(); assert_eq!(bucket.object_count, 1); assert_eq!(bucket.total_size, 1024); - - let obj = - S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").expect("object must be stored"); + let obj = S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg") + .expect("object must be stored"); assert_eq!(obj.cid, cid); assert_eq!(obj.size, 1024); + // 4. Delete the object — bucket goes back to empty. assert_ok_ok(construct_and_apply_extrinsic( Some(user.pair()), RuntimeCall::S3Registry(S3RegistryCall::::delete_object_metadata { @@ -1164,51 +1054,18 @@ fn should_put_and_delete_object_metadata() { key: b"photos/cat.jpg".to_vec(), }), )); - - assert!(S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").is_none()); let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id).unwrap(); assert_eq!(bucket.object_count, 0); assert_eq!(bucket.total_size, 0); - }); -} - -#[test] -fn should_create_s3_bucket_with_storage() { - new_test_ext().execute_with(|| { - advance_block(); - - let provider = Sr25519Keyring::Bob; - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - - register_accepting_provider_for(provider, default_stake()); - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - - let s3_bucket_id_before = pallet_s3_registry::NextS3BucketId::::get(); + assert!(S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").is_none()); + // 5. Delete the empty bucket. assert_ok_ok(construct_and_apply_extrinsic( Some(user.pair()), - RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket_with_storage { - name: b"storage-bucket".to_vec(), - max_capacity: 1_000_000, - duration: 500, - max_payment: UNIT, - }), - )); - - let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id_before) - .expect("bucket must be stored"); - assert_eq!(bucket.owner, user_id); - assert_eq!(bucket.name.as_slice(), b"storage-bucket"); - - System::assert_has_event(RuntimeEvent::S3Registry( - pallet_s3_registry::Event::S3BucketCreated { - s3_bucket_id: s3_bucket_id_before, - name: b"storage-bucket".to_vec(), - layer0_bucket_id: bucket.layer0_bucket_id, - owner: user_id, - }, + RuntimeCall::S3Registry(S3RegistryCall::::delete_s3_bucket { s3_bucket_id }), )); + assert!(pallet_s3_registry::S3Buckets::::get(s3_bucket_id).is_none()); + assert!(!pallet_s3_registry::UserBuckets::::get(&user_id).contains(&s3_bucket_id)); }); } @@ -1216,95 +1073,105 @@ fn should_create_s3_bucket_with_storage() { // Tests for the S3 registry pallet via XCM. // =============================== +/// End-to-end S3 bucket lifecycle dispatched via XCM from a sibling +/// parachain: each call's `Signed` origin is the *derived sovereign* of +/// `(Parachain(N), AccountId32(Alice))`, so the provider must sign terms +/// with `terms.owner = derived` (not Alice's keyring account). #[test] -fn should_create_s3_bucket_with_storage_via_xcm() { - let user = Sr25519Keyring::Alice; - let user_id: AccountId = user.to_account_id(); - - let create_call = - RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket_with_storage { - name: b"xcm-bucket".to_vec(), - max_capacity: 1_000_000, - duration: 500, - max_payment: UNIT, - }); +fn s3_bucket_lifecycle_via_xcm_e2e() { + let alice_on_para = alice_on_sibling_parachain(5_000); + let provider = Sr25519Keyring::Bob; xcm_test_ext().execute_with(|| { - // Bob is the storage provider; Alice creates the S3 bucket via XCM. - register_accepting_provider_for(Sr25519Keyring::Bob, default_stake()); - - let _ = Balances::deposit_creating(&user_id, 10 * UNIT); - - let s3_bucket_id_before = pallet_s3_registry::NextS3BucketId::::get(); + // Derive the sovereign account that XCM dispatch will use as `Signed`. + let derived: AccountId = + LocationToAccountHelper::::convert_location( + alice_on_para.clone().into(), + ) + .expect("Alice-on-para must convert to an account"); - // Alice's local account location maps directly to her AccountId. - let alice_location = Location::new( - 0, - [Junction::AccountId32 { - network: None, - id: user.to_raw_public(), - }], - ); + // 1. Provider setup + fund the derived account for fees and storage. + register_accepting_provider_for(provider, default_stake()); + let _ = Balances::deposit_creating(&derived, 10 * UNIT); + let s3_bucket_id = pallet_s3_registry::NextS3BucketId::::get(); let fee: Asset = (Location::parent(), UNIT).into(); + // 2. Create the bucket: provider signs terms for `derived`, XCM + // dispatches the call from `alice_on_para`. + let terms = primary_terms(derived.clone(), 1_000_000, 500, 1); + let sig = sign_primary_terms(provider, &terms); + let create_call = RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket { + name: b"sibling-bucket".to_vec(), + provider: provider.to_account_id(), + terms, + sig, + }); assert_ok!( RuntimeHelper::::execute_as_origin( - (alice_location, OriginKind::SovereignAccount), + (alice_on_para.clone(), OriginKind::SovereignAccount), create_call, - Some(fee), + Some(fee.clone()), ) .ensure_complete() ); - let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id_before) + let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id) .expect("bucket must be stored"); - assert_eq!(bucket.owner, user_id); - assert_eq!(bucket.name.as_slice(), b"xcm-bucket"); - }); -} - -#[test] -fn should_create_s3_bucket_with_storage_via_xcm_from_sibling_parachain() { - // Use a distinct para id from all other sibling-parachain tests. - let alice_on_para = alice_on_sibling_parachain(5_000); + assert_eq!(bucket.owner, derived); + assert_eq!(bucket.name.as_slice(), b"sibling-bucket"); + assert!(pallet_s3_registry::UserBuckets::::get(&derived).contains(&s3_bucket_id)); - let create_call = - RuntimeCall::S3Registry(S3RegistryCall::::create_s3_bucket_with_storage { - name: b"sibling-bucket".to_vec(), - max_capacity: 1_000_000, - duration: 500, - max_payment: UNIT, + // 3. Put an object via XCM. + let cid = sp_core::H256::repeat_byte(0xAB); + let put_call = RuntimeCall::S3Registry(S3RegistryCall::::put_object_metadata { + s3_bucket_id, + key: b"photos/cat.jpg".to_vec(), + cid, + size: 1024, + content_type: b"image/jpeg".to_vec(), + user_metadata: vec![], }); - - xcm_test_ext().execute_with(|| { - let derived: AccountId = - LocationToAccountHelper::::convert_location( - alice_on_para.clone().into(), + assert_ok!( + RuntimeHelper::::execute_as_origin( + (alice_on_para.clone(), OriginKind::SovereignAccount), + put_call, + Some(fee.clone()), ) - .expect("Alice-on-para must convert to an account"); - - register_accepting_provider_for(Sr25519Keyring::Bob, default_stake()); - let _ = Balances::deposit_creating(&derived, 10 * UNIT); + .ensure_complete() + ); + assert_eq!( + S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").unwrap().cid, + cid + ); - let s3_bucket_id_before = pallet_s3_registry::NextS3BucketId::::get(); - let fee: Asset = (Location::parent(), UNIT).into(); + // 4. Delete the object via XCM. + let delete_obj_call = + RuntimeCall::S3Registry(S3RegistryCall::::delete_object_metadata { + s3_bucket_id, + key: b"photos/cat.jpg".to_vec(), + }); + assert_ok!( + RuntimeHelper::::execute_as_origin( + (alice_on_para.clone(), OriginKind::SovereignAccount), + delete_obj_call, + Some(fee.clone()), + ) + .ensure_complete() + ); + assert!(S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").is_none()); + // 5. Delete the empty bucket via XCM. + let delete_bucket_call = + RuntimeCall::S3Registry(S3RegistryCall::::delete_s3_bucket { s3_bucket_id }); assert_ok!( RuntimeHelper::::execute_as_origin( (alice_on_para, OriginKind::SovereignAccount), - create_call, + delete_bucket_call, Some(fee), ) .ensure_complete() ); - - let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id_before) - .expect("bucket must be stored"); - assert_eq!(bucket.owner, derived); - assert_eq!(bucket.name.as_slice(), b"sibling-bucket"); - - let user_buckets = pallet_s3_registry::UserBuckets::::get(&derived); - assert!(user_buckets.contains(&s3_bucket_id_before)); + assert!(pallet_s3_registry::S3Buckets::::get(s3_bucket_id).is_none()); }); } diff --git a/storage-interfaces/file-system/pallet-registry/src/weights.rs b/storage-interfaces/file-system/pallet-registry/src/weights.rs index a5bc8cbd..7eedfccc 100644 --- a/storage-interfaces/file-system/pallet-registry/src/weights.rs +++ b/storage-interfaces/file-system/pallet-registry/src/weights.rs @@ -19,7 +19,7 @@ //! Autogenerated weights for `pallet_drive_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-11, STEPS: `10`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `10`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -65,87 +65,87 @@ pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:5 w:5) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::NextDriveId` (r:1 w:1) /// Proof: `DriveRegistry::NextDriveId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::Drives` (r:0 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `2351` - // Estimated: `17970` - // Minimum execution time: 123_000_000 picoseconds. - Weight::from_parts(134_000_000, 17970) - .saturating_add(T::DbWeight::get().reads(16_u64)) - .saturating_add(T::DbWeight::get().writes(13_u64)) + // Measured: `1301` + // Estimated: `11515` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(64_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:6 w:5) + /// Storage: `StorageProvider::StorageAgreements` (r:2 w:1) /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:6 w:6) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:5 w:5) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:100 w:100) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:0) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn delete_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `14780` + // Measured: `12052` // Estimated: `1053490` - // Minimum execution time: 489_000_000 picoseconds. - Weight::from_parts(529_000_000, 1053490) - .saturating_add(T::DbWeight::get().reads(121_u64)) - .saturating_add(T::DbWeight::get().writes(120_u64)) + // Minimum execution time: 331_000_000 picoseconds. + Weight::from_parts(367_000_000, 1053490) + .saturating_add(T::DbWeight::get().reads(108_u64)) + .saturating_add(T::DbWeight::get().writes(108_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn share_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4813` + // Measured: `4757` // Estimated: `11515` // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 11515) + Weight::from_parts(21_000_000, 11515) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn unshare_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4840` + // Measured: `4784` // Estimated: `11515` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 11515) + // Minimum execution time: 19_000_000 picoseconds. + Weight::from_parts(22_000_000, 11515) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -155,87 +155,87 @@ impl WeightInfo for SubstrateWeight { impl WeightInfo for () { /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:5 w:5) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::NextDriveId` (r:1 w:1) /// Proof: `DriveRegistry::NextDriveId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::Drives` (r:0 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `2351` - // Estimated: `17970` - // Minimum execution time: 123_000_000 picoseconds. - Weight::from_parts(134_000_000, 17970) - .saturating_add(RocksDbWeight::get().reads(16_u64)) - .saturating_add(RocksDbWeight::get().writes(13_u64)) + // Measured: `1301` + // Estimated: `11515` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(64_000_000, 11515) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(11_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:6 w:5) + /// Storage: `StorageProvider::StorageAgreements` (r:2 w:1) /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:6 w:6) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:5 w:5) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:100 w:100) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:0) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn delete_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `14780` + // Measured: `12052` // Estimated: `1053490` - // Minimum execution time: 489_000_000 picoseconds. - Weight::from_parts(529_000_000, 1053490) - .saturating_add(RocksDbWeight::get().reads(121_u64)) - .saturating_add(RocksDbWeight::get().writes(120_u64)) + // Minimum execution time: 331_000_000 picoseconds. + Weight::from_parts(367_000_000, 1053490) + .saturating_add(RocksDbWeight::get().reads(108_u64)) + .saturating_add(RocksDbWeight::get().writes(108_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn share_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4813` + // Measured: `4757` // Estimated: `11515` // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 11515) + Weight::from_parts(21_000_000, 11515) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn unshare_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4840` + // Measured: `4784` // Estimated: `11515` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 11515) + // Minimum execution time: 19_000_000 picoseconds. + Weight::from_parts(22_000_000, 11515) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } diff --git a/storage-interfaces/s3/pallet-s3-registry/src/weights.rs b/storage-interfaces/s3/pallet-s3-registry/src/weights.rs index be3ad2f6..28786e29 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/weights.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/weights.rs @@ -19,7 +19,7 @@ //! Autogenerated weights for `pallet_s3_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-11, STEPS: `10`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-28, STEPS: `10`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` @@ -59,7 +59,6 @@ pub trait WeightInfo { fn put_object_metadata() -> Weight; fn delete_object_metadata() -> Weight; fn copy_object_metadata() -> Weight; - fn create_s3_bucket_with_storage() -> Weight; } /// Weights for `pallet_s3_registry` using the Substrate node and recommended hardware. @@ -69,6 +68,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) /// Storage: `S3Registry::UserBuckets` (r:1 w:1) /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) @@ -79,14 +84,16 @@ impl WeightInfo for SubstrateWeight { /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_s3_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `1107` + // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 11515) - .saturating_add(T::DbWeight::get().reads(5_u64)) - .saturating_add(T::DbWeight::get().writes(7_u64)) + // Minimum execution time: 59_000_000 picoseconds. + Weight::from_parts(67_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -99,7 +106,7 @@ impl WeightInfo for SubstrateWeight { // Measured: `1169` // Estimated: `4315` // Minimum execution time: 11_000_000 picoseconds. - Weight::from_parts(12_000_000, 4315) + Weight::from_parts(20_000_000, 4315) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -111,8 +118,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 29_000_000 picoseconds. - Weight::from_parts(32_000_000, 11176) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(54_000_000, 11176) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -124,8 +131,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(23_000_000, 11176) + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(40_000_000, 11176) .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } @@ -137,40 +144,11 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `8205` // Estimated: `21362` - // Minimum execution time: 31_000_000 picoseconds. - Weight::from_parts(34_000_000, 21362) + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(58_000_000, 21362) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(2_u64)) } - /// Storage: `S3Registry::BucketNameToId` (r:1 w:1) - /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::UserBuckets` (r:1 w:1) - /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::NextS3BucketId` (r:1 w:1) - /// Proof: `S3Registry::NextS3BucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::S3Buckets` (r:0 w:1) - /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_s3_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `2351` - // Estimated: `17970` - // Minimum execution time: 56_000_000 picoseconds. - Weight::from_parts(62_000_000, 17970) - .saturating_add(T::DbWeight::get().reads(13_u64)) - .saturating_add(T::DbWeight::get().writes(9_u64)) - } } // For backwards compatibility and tests. @@ -179,6 +157,12 @@ impl WeightInfo for () { /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) /// Storage: `S3Registry::UserBuckets` (r:1 w:1) /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) @@ -189,14 +173,16 @@ impl WeightInfo for () { /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_s3_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `1107` + // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 11515) - .saturating_add(RocksDbWeight::get().reads(5_u64)) - .saturating_add(RocksDbWeight::get().writes(7_u64)) + // Minimum execution time: 59_000_000 picoseconds. + Weight::from_parts(67_000_000, 11515) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(11_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -209,7 +195,7 @@ impl WeightInfo for () { // Measured: `1169` // Estimated: `4315` // Minimum execution time: 11_000_000 picoseconds. - Weight::from_parts(12_000_000, 4315) + Weight::from_parts(20_000_000, 4315) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -221,8 +207,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 29_000_000 picoseconds. - Weight::from_parts(32_000_000, 11176) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(54_000_000, 11176) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -234,8 +220,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 20_000_000 picoseconds. - Weight::from_parts(23_000_000, 11176) + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(40_000_000, 11176) .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } @@ -247,38 +233,9 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `8205` // Estimated: `21362` - // Minimum execution time: 31_000_000 picoseconds. - Weight::from_parts(34_000_000, 21362) + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(58_000_000, 21362) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(2_u64)) } - /// Storage: `S3Registry::BucketNameToId` (r:1 w:1) - /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::UserBuckets` (r:1 w:1) - /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(355), added: 2830, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::NextS3BucketId` (r:1 w:1) - /// Proof: `S3Registry::NextS3BucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::S3Buckets` (r:0 w:1) - /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_s3_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `2351` - // Estimated: `17970` - // Minimum execution time: 56_000_000 picoseconds. - Weight::from_parts(62_000_000, 17970) - .saturating_add(RocksDbWeight::get().reads(13_u64)) - .saturating_add(RocksDbWeight::get().writes(9_u64)) - } } \ No newline at end of file From 82b591992a9048d4f6edf00095fd1ea54b09051e Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 29 May 2026 09:41:27 +0700 Subject: [PATCH 10/44] feat: rewire client SDK to signed-terms establish_storage_agreement - Add client/src/agreement.rs with the AgreementTermsOf mirror type, NegotiateRequest / SignedTerms wire shapes, a hex-bytes MultiSignature serde adapter, and a sign_terms helper that matches the on-chain blake2_256(SCALE(terms)) verification. - AdminClient: replace create_bucket + request_agreement + withdraw_agreement_request + terminate-style request/accept helpers with establish_storage_agreement(provider, terms, sig), which parses the new BucketCreated event to surface the bucket id. - ProviderClient: drop accept_agreement / list_pending_requests / reject_agreement_request; add mock negotiate_terms HTTP client that POSTs to a provider node `/negotiate` endpoint and returns SignedTerms. - Update complete_workflow.rs, and tests to the new flow. --- Cargo.lock | 1 + client/Cargo.toml | 1 + client/examples/complete_workflow.rs | 53 ++++---- client/src/admin.rs | 181 +++++++-------------------- client/src/agreement.rs | 96 ++++++++++++++ client/src/lib.rs | 35 +++--- client/src/provider.rs | 149 +++++++--------------- client/src/substrate.rs | 125 +++++++++--------- client/tests/admin_integration.rs | 19 +-- client/tests/common/mod.rs | 40 +++++- client/tests/provider_integration.rs | 61 --------- 11 files changed, 346 insertions(+), 415 deletions(-) create mode 100644 client/src/agreement.rs diff --git a/Cargo.lock b/Cargo.lock index 4c06ef73..e708a07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7838,6 +7838,7 @@ dependencies = [ "chacha20poly1305", "futures", "hex", + "parity-scale-codec", "rand 0.8.5", "reqwest", "serde", diff --git a/client/Cargo.toml b/client/Cargo.toml index 59f7bfdb..a6093a85 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,6 +9,7 @@ description = "Client library for scalable Web3 storage" [dependencies] storage-primitives = { workspace = true, features = ["serde", "std"] } +codec = { workspace = true, features = ["std"] } reqwest = { workspace = true } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true } diff --git a/client/examples/complete_workflow.rs b/client/examples/complete_workflow.rs index 1cc5fd0b..7342304e 100644 --- a/client/examples/complete_workflow.rs +++ b/client/examples/complete_workflow.rs @@ -2,16 +2,16 @@ //! //! This example shows: //! 1. Provider registration -//! 2. Bucket creation by admin -//! 3. Agreement request and acceptance +//! 2. Negotiating provider-signed terms over HTTP +//! 3. Redeeming the signed terms on-chain to open a bucket + agreement //! 4. Data upload by storage user //! 5. Checkpoint creation //! 6. Challenge by third party //! 7. Challenge response by provider use storage_client::{ - AdminClient, ChallengerClient, ChunkingStrategy, ClientConfig, ProviderClient, - StorageUserClient, + AdminClient, ChallengerClient, ChunkingStrategy, ClientConfig, NegotiateRequest, + ProviderClient, StorageUserClient, }; #[tokio::main] @@ -48,38 +48,43 @@ async fn main() -> Result<(), Box> { println!(" ✓ Provider registered with 10 tokens stake\n"); // ═════════════════════════════════════════════════════════════════════════ - // Step 2: Bucket Creation by Admin + // Step 2: Negotiate signed terms with the provider // ═════════════════════════════════════════════════════════════════════════ - println!("🗂️ Step 2: Bucket Creation"); + println!("🤝 Step 2: Negotiating storage terms"); let admin_account = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; let admin_client = AdminClient::new(config.clone(), admin_account.to_string())?; - println!(" Creating bucket with min_providers=1..."); - let bucket_id = admin_client.create_bucket(1).await?; - println!(" ✓ Bucket created with ID: {bucket_id}\n"); + let provider_http_url = config.provider_urls[0].clone(); + println!(" Asking provider for signed terms over HTTP..."); + let signed = ProviderClient::negotiate_terms( + &provider_http_url, + &NegotiateRequest { + owner: admin_account.parse()?, + max_bytes: 10 * 1024 * 1024 * 1024, // 10 GB + duration: 100_000, // ~2 weeks at 6 sec blocks + valid_until_offset: 1_000, + }, + ) + .await?; + println!( + " ✓ Provider signed terms: max_bytes={}, duration={}, nonce={}", + signed.terms.max_bytes, signed.terms.duration, signed.terms.nonce + ); // ═════════════════════════════════════════════════════════════════════════ - // Step 3: Agreement Request and Acceptance + // Step 3: Redeem signed terms on-chain (bucket + agreement atomically) // ═════════════════════════════════════════════════════════════════════════ - println!("🤝 Step 3: Storage Agreement"); + println!("📦 Step 3: Establishing storage agreement on-chain"); - println!(" Admin requesting storage agreement..."); - admin_client - .request_agreement( - bucket_id, + let bucket_id = admin_client + .establish_storage_agreement( provider_account.to_string(), - 10 * 1024 * 1024 * 1024, // 10 GB - 100_000, // ~2 weeks at 6 sec blocks - 5_000_000_000_000, // 5 tokens payment - None, // Primary (not replica) + signed.terms, + signed.signature, ) .await?; - println!(" ✓ Agreement requested: 10 GB for 100,000 blocks"); - - println!(" Provider accepting agreement..."); - provider_client.accept_agreement(bucket_id).await?; - println!(" ✓ Agreement accepted!\n"); + println!(" ✓ Bucket {bucket_id} created + primary agreement opened\n"); // ═════════════════════════════════════════════════════════════════════════ // Step 4: Data Upload by Storage User diff --git a/client/src/admin.rs b/client/src/admin.rs index a1cda547..380ecdb4 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -1,17 +1,18 @@ //! Admin Client - For bucket administrators managing buckets and agreements. //! //! This client provides operations for: -//! - Creating and configuring buckets +//! - Redeeming provider-signed terms to open a bucket + primary agreement //! - Managing bucket members and permissions -//! - Requesting storage agreements from providers -//! - Terminating agreements +//! - Extending / topping up / terminating agreements //! - Freezing buckets //! - Deleting old data +use crate::agreement::AgreementTermsOf; use crate::base::{BaseClient, ClientConfig, ClientError, ClientResult}; use crate::event_subscription::{EventParser, StorageEvent, StorageProviderEventParser}; use crate::substrate::{extrinsics, storage, SubstrateClient}; use sp_core::H256; +use sp_runtime::MultiSignature; use storage_primitives::{BucketId, EndAction, Role}; use subxt::ext::scale_value::{Composite, ValueDef, Variant}; @@ -50,36 +51,58 @@ impl AdminClient { // Bucket Management // ═════════════════════════════════════════════════════════════════════════ - /// Create a new storage bucket. + /// Redeem provider-signed terms to open a bucket + primary agreement + /// atomically. /// - /// # Parameters - /// - `min_providers`: Minimum number of provider signatures required for checkpoints + /// `terms` and `sig` come from the provider — typically via + /// [`ProviderClient::negotiate_terms`](crate::provider::ProviderClient::negotiate_terms), + /// but any source that produces a valid signature works. /// - /// # Returns - /// The bucket ID of the newly created bucket. + /// On success, returns the new bucket id (parsed from the + /// `BucketCreated` event emitted by the runtime). /// /// # Example /// ```no_run - /// # use storage_client::AdminClient; + /// # use storage_client::{AdminClient, ProviderClient, NegotiateRequest}; /// # async fn example() -> Result<(), Box> { /// let client = AdminClient::with_defaults("5GrwvaEF...".to_string())?; - /// let bucket_id = client.create_bucket(2).await?; - /// println!("Created bucket {}", bucket_id); + /// let signed = ProviderClient::negotiate_terms( + /// "http://provider.example:3333", + /// &NegotiateRequest { + /// owner: "5GrwvaEF...".parse()?, + /// max_bytes: 1_000_000, + /// duration: 100, + /// valid_until_offset: 1_000, + /// }, + /// ).await?; + /// let bucket_id = client.establish_storage_agreement( + /// "5FHneW46...".to_string(), + /// signed.terms, + /// signed.signature, + /// ).await?; /// # Ok(()) /// # } /// ``` - pub async fn create_bucket(&self, min_providers: u32) -> ClientResult { + pub async fn establish_storage_agreement( + &self, + provider: String, + terms: AgreementTermsOf, + sig: MultiSignature, + ) -> ClientResult { let chain = self.base.chain()?; let signer = chain.signer()?; + let provider_account = SubstrateClient::parse_account(&provider)?; tracing::info!( - "Creating bucket with min_providers={} for admin {}", - min_providers, - self.admin_account + "Establishing storage agreement with provider {} for owner {} (max_bytes={}, duration={}, nonce={})", + provider, + self.admin_account, + terms.max_bytes, + terms.duration, + terms.nonce, ); - // Create and submit the extrinsic - let tx = extrinsics::create_bucket(min_providers); + let tx = extrinsics::establish_storage_agreement(provider_account, &terms, &sig); let tx_progress = chain .api() @@ -88,22 +111,16 @@ impl AdminClient { .await .map_err(|e| ClientError::Chain(format!("Failed to submit tx: {e}")))?; - // Wait for finalization, capture the block hash, then check success. let tx_in_block = tx_progress .wait_for_finalized() .await .map_err(|e| ClientError::Chain(format!("Transaction failed: {e}")))?; - let raw_block_hash = tx_in_block.block_hash(); - let events = tx_in_block .wait_for_success() .await .map_err(|e| ClientError::Chain(format!("Transaction failed: {e}")))?; - tracing::info!("Bucket created successfully"); - - // Convert subxt's H256 (primitive_types 0.12) to sp_core::H256 (0.13) via raw bytes. let block_hash = H256::from_slice(raw_block_hash.as_bytes()); let block_number = chain .api() @@ -116,12 +133,13 @@ impl AdminClient { let parsed = StorageProviderEventParser::from_extrinsic_events(&events, block_hash, block_number); - tracing::info!("Parsed events: {:?}", parsed); - for event in parsed { - tracing::info!("Received event: {event:?}"); if let StorageEvent::BucketCreated { bucket_id, .. } = event { - tracing::info!("Bucket created with ID: {bucket_id}"); + tracing::info!( + "Storage agreement established; bucket {} created with provider {}", + bucket_id, + provider, + ); return Ok(bucket_id); } } @@ -247,107 +265,8 @@ impl AdminClient { // Agreement Management // ═════════════════════════════════════════════════════════════════════════ - /// Request a storage agreement from a provider. - /// - /// # Parameters - /// - `provider`: Provider's account ID - /// - `max_bytes`: Maximum storage capacity to reserve - /// - `duration`: Agreement duration in blocks - /// - `payment`: Total payment to lock (will be released to provider on success) - /// /// # Example /// ```no_run - /// # use storage_client::AdminClient; - /// # async fn example() -> Result<(), Box> { - /// let client = AdminClient::with_defaults("5GrwvaEF...".to_string())?; - /// let provider = "5FHneW46...".to_string(); - /// - /// client.request_agreement( - /// 1, // bucket_id - /// provider, - /// 10 * 1024 * 1024 * 1024, // 10 GB - /// 100_000, // duration blocks (~2 weeks) - /// 1_000_000_000_000, // payment - /// None // primary (not replica) - /// ).await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn request_agreement( - &self, - bucket_id: BucketId, - provider: String, - max_bytes: u64, - duration: u32, - payment: u128, - replica_params: Option, - ) -> ClientResult<()> { - let chain = self.base.chain()?; - let signer = chain.signer()?; - - tracing::info!( - "Requesting agreement from {} for bucket {}: {} bytes, {} blocks, {} payment", - provider, - bucket_id, - max_bytes, - duration, - payment - ); - - // Parse provider account - let provider_account = SubstrateClient::parse_account(&provider)?; - - // Use different extrinsic based on whether it's a primary or replica agreement - if let Some(params) = replica_params { - // Replica agreement - let tx = extrinsics::request_agreement( - bucket_id, - provider_account, - max_bytes, - duration, - payment, - params.sync_balance, - params.min_sync_interval, - ); - - let tx_progress = chain - .api() - .tx() - .sign_and_submit_then_watch_default(&tx, signer) - .await - .map_err(|e| ClientError::Chain(format!("Failed to submit tx: {e}")))?; - - tx_progress - .wait_for_finalized_success() - .await - .map_err(|e| ClientError::Chain(format!("Transaction failed: {e}")))?; - } else { - // Primary agreement - let tx = extrinsics::request_primary_agreement( - bucket_id, - provider_account, - max_bytes, - duration, - payment, - ); - - let tx_progress = chain - .api() - .tx() - .sign_and_submit_then_watch_default(&tx, signer) - .await - .map_err(|e| ClientError::Chain(format!("Failed to submit tx: {e}")))?; - - tx_progress - .wait_for_finalized_success() - .await - .map_err(|e| ClientError::Chain(format!("Transaction failed: {e}")))?; - } - - tracing::info!("Agreement request submitted successfully"); - Ok(()) - } - /// Extend an existing agreement with a provider. /// /// Anyone can extend if price hasn't increased (permissionless persistence). @@ -832,16 +751,6 @@ fn collect_bytes_recursive(value: &subxt::ext::scale_value::Value, buf: &mu // Types -#[derive(Debug, Clone)] -pub struct ReplicaParams { - /// The primary provider this replica syncs from - pub primary_provider: Option, - /// Initial sync balance to fund per-sync payments - pub sync_balance: u128, - /// Minimum blocks between sync confirmations - pub min_sync_interval: u32, -} - #[derive(Debug, Clone)] pub struct BucketInfo { pub bucket_id: BucketId, diff --git a/client/src/agreement.rs b/client/src/agreement.rs new file mode 100644 index 00000000..277498da --- /dev/null +++ b/client/src/agreement.rs @@ -0,0 +1,96 @@ +//! Provider-signed agreement terms — wire format + client-side signing helper. +//! +//! Bucket creation went from a request/accept handshake to a single +//! `establish_storage_agreement` redemption of provider-signed +//! [`AgreementTerms`]. This module holds: +//! +//! * The runtime-specific [`AgreementTermsOf`] alias the client uses to +//! talk to the parachain (AccountId32 / u128 / u32). +//! * [`SignedTerms`] — the negotiated bundle returned over HTTP by a +//! provider node's `/negotiate` endpoint. +//! * [`NegotiateRequest`] — the JSON body that bucket owners POST to +//! `/negotiate`. +//! * [`sign_terms`] — a helper for provider-side code (tests, fixtures) +//! that need to sign terms without going through the full provider +//! keystore. +//! +//! The on-chain pallet hashes `blake2_256(SCALE(terms))` and verifies the +//! signature against the provider's registered public key, so the same +//! encoding has to be used on both sides — `sign_terms` enforces that. + +use codec::Encode; +use serde::{Deserialize, Serialize}; +use sp_core::hashing::blake2_256; +use sp_runtime::{AccountId32, MultiSignature}; +use storage_primitives::AgreementTerms; + +/// Concrete [`AgreementTerms`] type for the storage parachain. +/// +/// Balance is `u128`, BlockNumber is `u32`; matches `parachains_common`'s +/// runtime types used by `storage_paseo_runtime`. +pub type AgreementTermsOf = AgreementTerms; + +/// Request body that a bucket owner POSTs to a provider node's +/// `/negotiate` endpoint. +/// +/// The provider node turns this into [`AgreementTerms`] (filling in +/// `price_per_byte` from its own settings, choosing a `nonce`, and +/// resolving `valid_until = current_block + valid_until_offset`), signs +/// the SCALE encoding, and returns [`SignedTerms`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NegotiateRequest { + /// The account that will own the resulting bucket / drive. Must + /// match the `Signed` origin that later submits + /// `establish_storage_agreement`. + pub owner: AccountId32, + /// Storage quota requested, in bytes. + pub max_bytes: u64, + /// Agreement duration in blocks from activation. + pub duration: u32, + /// How many blocks the resulting quote should be valid for. The + /// provider chooses `valid_until = current_block + valid_until_offset` + /// so the owner has a bounded window to redeem it on-chain. + pub valid_until_offset: u32, +} + +/// Provider-signed agreement terms ready to be redeemed via +/// `establish_storage_agreement`. +/// +/// `signature` is a `MultiSignature` over `blake2_256(SCALE(terms))`, +/// produced by the provider's registered key. We carry the signature as +/// hex over the wire — `MultiSignature` doesn't derive serde directly, +/// and hex keeps the JSON readable. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedTerms { + pub terms: AgreementTermsOf, + #[serde(with = "hex_multi_signature")] + pub signature: MultiSignature, +} + +/// Sign already-built terms with a provider keypair. +/// +/// Mirror of the on-chain `verify_terms_signature`: SCALE-encode, hash +/// with blake2-256, then sign. The runtime accepts `MultiSignature`, so +/// callers wrap the raw sr25519 signature with `MultiSignature::Sr25519`. +pub fn sign_terms(keypair: &subxt_signer::sr25519::Keypair, terms: &AgreementTermsOf) -> MultiSignature { + let hash = blake2_256(&terms.encode()); + let raw = keypair.sign(&hash); + MultiSignature::Sr25519(sp_core::sr25519::Signature::from_raw(raw.0)) +} + +/// Hex-bytes serde adapter for [`MultiSignature`] — SCALE-encode then hex. +mod hex_multi_signature { + use codec::{Decode, Encode}; + use serde::{Deserialize, Deserializer, Serializer}; + use sp_runtime::MultiSignature; + + pub fn serialize(sig: &MultiSignature, s: S) -> Result { + s.serialize_str(&hex::encode(sig.encode())) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = String::deserialize(d)?; + let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?; + MultiSignature::decode(&mut &bytes[..]).map_err(serde::de::Error::custom) + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 3e2a744c..02bd5c03 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -42,9 +42,6 @@ //! vec![0u8; 32], // public key //! 1_000_000_000_000, // stake //! ).await?; -//! -//! // Accept agreements -//! client.accept_agreement(1).await?; //! # Ok(()) //! # } //! ``` @@ -52,22 +49,28 @@ //! ### For Bucket Administrators //! [`AdminClient`](admin::AdminClient) - Manage buckets and agreements //! ```no_run -//! use storage_client::{AdminClient, ClientConfig}; +//! use storage_client::{AdminClient, ProviderClient, NegotiateRequest}; //! //! # async fn example() -> Result<(), Box> { //! let client = AdminClient::with_defaults("5GrwvaEF...".to_string())?; //! -//! // Create bucket -//! let bucket_id = client.create_bucket(2).await?; -//! -//! // Request storage -//! client.request_agreement( -//! bucket_id, -//! "5FHneW46...".to_string(), -//! 10 * 1024 * 1024 * 1024, // 10 GB -//! 100_000, // blocks -//! 1_000_000_000_000, // payment -//! None, +//! // 1. Negotiate signed terms with the provider over HTTP. +//! let signed = ProviderClient::negotiate_terms( +//! "http://provider.example:3333", +//! &NegotiateRequest { +//! owner: "5GrwvaEF...".parse()?, +//! max_bytes: 10 * 1024 * 1024 * 1024, // 10 GB +//! duration: 100_000, +//! valid_until_offset: 1_000, +//! }, +//! ).await?; +//! +//! // 2. Redeem them on-chain — bucket creation + primary agreement +//! // happen atomically inside `establish_storage_agreement`. +//! let bucket_id = client.establish_storage_agreement( +//! "5FHneW46...".to_string(), // provider account +//! signed.terms, +//! signed.signature, //! ).await?; //! # Ok(()) //! # } @@ -94,6 +97,7 @@ // Re-export main types pub mod admin; +pub mod agreement; pub mod base; pub mod challenger; pub mod checkpoint; @@ -109,6 +113,7 @@ pub mod verification; // Re-export commonly used types pub use admin::AdminClient; +pub use agreement::{sign_terms, AgreementTermsOf, NegotiateRequest, SignedTerms}; pub use base::{ChunkingStrategy, ClientConfig, ClientError, ClientResult}; pub use challenger::ChallengerClient; pub use checkpoint::{ diff --git a/client/src/provider.rs b/client/src/provider.rs index 5db74a06..0295b052 100644 --- a/client/src/provider.rs +++ b/client/src/provider.rs @@ -307,126 +307,63 @@ impl ProviderClient { } // ═════════════════════════════════════════════════════════════════════════ - // Agreement Management + // Term Negotiation (off-chain) // ═════════════════════════════════════════════════════════════════════════ - /// Accept a storage agreement request for a bucket. + /// Negotiate provider-signed agreement terms over HTTP. /// - /// This commits you to storing data for the specified bucket. + /// The bucket owner POSTs the request shape they want; the provider + /// node returns SCALE-encoded [`AgreementTerms`](storage_primitives::AgreementTerms) + /// signed with its registered key. The result is fed directly into + /// [`AdminClient::establish_storage_agreement`](crate::admin::AdminClient::establish_storage_agreement) + /// to open the bucket + primary agreement on-chain. + /// + /// This is the on-ramp that replaced the old `request_agreement` + + /// `accept_agreement` two-step. + /// + /// # Parameters + /// - `provider_url`: Base HTTP URL of the provider node (no trailing slash). + /// - `req`: The negotiation request (owner, capacity, duration, validity). /// /// # Example /// ```no_run - /// # use storage_client::ProviderClient; + /// # use storage_client::{ProviderClient, NegotiateRequest}; /// # async fn example() -> Result<(), Box> { - /// let client = ProviderClient::with_defaults("5GrwvaEF...".to_string())?; - /// client.accept_agreement(1).await?; - /// println!("Agreement accepted!"); + /// let signed = ProviderClient::negotiate_terms( + /// "http://provider.example:3333", + /// &NegotiateRequest { + /// owner: "5GrwvaEF...".parse()?, + /// max_bytes: 10 * 1024 * 1024 * 1024, + /// duration: 100_000, + /// valid_until_offset: 1_000, + /// }, + /// ).await?; /// # Ok(()) /// # } /// ``` - pub async fn accept_agreement(&self, bucket_id: BucketId) -> ClientResult<()> { - let chain = self.base.chain()?; - let signer = chain.signer()?; - - tracing::info!( - "Accepting agreement for bucket {} as provider {}", - bucket_id, - self.provider_account - ); - - // Create and submit the extrinsic - let tx = extrinsics::accept_agreement(bucket_id); - - let tx_progress = chain - .api() - .tx() - .sign_and_submit_then_watch_default(&tx, signer) - .await - .map_err(|e| ClientError::Chain(format!("Failed to submit tx: {e}")))?; - - tx_progress - .wait_for_finalized_success() - .await - .map_err(|e| ClientError::Chain(format!("Transaction failed: {e}")))?; - - tracing::info!("Agreement accepted successfully"); - Ok(()) - } - - /// List all pending agreement requests for this provider. - pub async fn list_pending_requests(&self) -> ClientResult> { - let chain = self.base.chain()?; - let provider_account = SubstrateClient::parse_account(&self.provider_account) - .map_err(|e| ClientError::Chain(format!("Invalid provider account: {e}")))?; - - let storage = chain - .api() - .storage() - .at_latest() - .await - .map_err(|e| ClientError::Chain(format!("Failed to get storage: {e}")))?; - - let mut iter = storage - .iter(storage::agreement_requests_for_provider(&provider_account)) + pub async fn negotiate_terms( + provider_url: &str, + req: &crate::agreement::NegotiateRequest, + ) -> ClientResult { + let url = format!("{}/negotiate", provider_url.trim_end_matches('/')); + let response = reqwest::Client::new() + .post(&url) + .json(req) + .send() .await - .map_err(|e| ClientError::Chain(format!("Failed to iterate requests: {e}")))?; + .map_err(ClientError::Http)?; - let mut requests = Vec::new(); - - while let Some(result) = iter.next().await { - let kv = - result.map_err(|e| ClientError::Chain(format!("Storage iteration error: {e}")))?; - - // bucket_id is encoded at offset 96 in the full storage key - // layout: [twox128(pallet)=16][twox128(storage)=16][blake2_128(provider)=16] - // [provider=32][blake2_128(bucket_id)=16][bucket_id=8] - let key = &kv.key_bytes; - if key.len() < 104 { - continue; - } - let bucket_id = - u64::from_le_bytes(key[96..104].try_into().unwrap_or([0u8; 8])) as BucketId; - - let value = match kv.value.to_value() { - Ok(v) => v, - Err(e) => { - tracing::warn!("Failed to decode agreement request: {e}"); - continue; - } - }; - - let requester = named_field(&value, "requester") - .and_then(decode_account_bytes) - .map(|b| format!("0x{}", hex::encode(b))) - .unwrap_or_default(); - - let max_bytes = named_field(&value, "max_bytes") - .and_then(|v| v.as_u128()) - .unwrap_or(0) as u64; - - let payment_locked = named_field(&value, "payment_locked") - .and_then(|v| v.as_u128()) - .unwrap_or(0); - - let duration = named_field(&value, "duration") - .and_then(|v| v.as_u128()) - .unwrap_or(0) as u32; - - let expires_at = named_field(&value, "expires_at") - .and_then(|v| v.as_u128()) - .unwrap_or(0) as u32; - - requests.push(AgreementRequest { - bucket_id, - requester, - max_bytes, - payment_locked, - duration, - expires_at, - }); + if !response.status().is_success() { + return Err(ClientError::Chain(format!( + "provider node rejected /negotiate with status {}", + response.status() + ))); } - Ok(requests) + response + .json::() + .await + .map_err(ClientError::Http) } /// List all active agreements for this provider. diff --git a/client/src/substrate.rs b/client/src/substrate.rs index 97826780..31f1e8c7 100644 --- a/client/src/substrate.rs +++ b/client/src/substrate.rs @@ -184,73 +184,78 @@ pub mod extrinsics { ) } - /// Create an accept_agreement extrinsic payload. - pub fn accept_agreement(bucket_id: u64) -> impl Payload { - subxt::dynamic::tx( - PALLET_NAME, - "accept_agreement", - vec![subxt::dynamic::Value::u128(bucket_id as u128)], - ) - } - - /// Create a create_bucket extrinsic payload. - pub fn create_bucket(min_providers: u32) -> impl Payload { - subxt::dynamic::tx( - PALLET_NAME, - "create_bucket", - vec![subxt::dynamic::Value::u128(min_providers as u128)], - ) - } - - /// Create a request_primary_agreement extrinsic payload (admin only). - pub fn request_primary_agreement( - bucket_id: u64, + /// Build an `establish_storage_agreement` extrinsic payload. + /// + /// Bundles the SCALE-encoded provider-signed terms and signature into + /// the dynamic call shape Layer 0 expects. The chain hashes + /// `blake2_256(SCALE(terms))` and verifies the signature against the + /// provider's registered public key. + pub fn establish_storage_agreement( provider: AccountId32, - max_bytes: u64, - duration: u32, - payment: u128, + terms: &crate::agreement::AgreementTermsOf, + sig: &sp_runtime::MultiSignature, ) -> impl Payload { - subxt::dynamic::tx( - PALLET_NAME, - "request_primary_agreement", - vec![ - subxt::dynamic::Value::u128(bucket_id as u128), - subxt::dynamic::Value::from_bytes(provider.as_ref() as &[u8]), - subxt::dynamic::Value::u128(max_bytes as u128), - subxt::dynamic::Value::u128(duration as u128), - subxt::dynamic::Value::u128(payment), - ], - ) - } + use codec::Encode; + let replica_params_value = match &terms.replica_params { + None => subxt::dynamic::Value::unnamed_variant("None", vec![]), + Some(rp) => subxt::dynamic::Value::unnamed_variant( + "Some", + vec![subxt::dynamic::Value::named_composite([ + ("sync_balance", subxt::dynamic::Value::u128(rp.sync_balance)), + ( + "min_sync_interval", + subxt::dynamic::Value::u128(rp.min_sync_interval as u128), + ), + ])], + ), + }; + let terms_value = subxt::dynamic::Value::named_composite([ + ( + "owner", + subxt::dynamic::Value::from_bytes(terms.owner.as_ref() as &[u8]), + ), + ("max_bytes", subxt::dynamic::Value::u128(terms.max_bytes as u128)), + ("duration", subxt::dynamic::Value::u128(terms.duration as u128)), + ( + "price_per_byte", + subxt::dynamic::Value::u128(terms.price_per_byte), + ), + ( + "valid_until", + subxt::dynamic::Value::u128(terms.valid_until as u128), + ), + ("nonce", subxt::dynamic::Value::u128(terms.nonce as u128)), + ("replica_params", replica_params_value), + ]); + + // MultiSignature is a SCALE enum; surface it as `Sr25519` for the + // current signer (the only variant the provider node emits today). + let sig_value = match sig { + sp_runtime::MultiSignature::Sr25519(s) => subxt::dynamic::Value::unnamed_variant( + "Sr25519", + vec![subxt::dynamic::Value::from_bytes(s.encode())], + ), + sp_runtime::MultiSignature::Ed25519(s) => subxt::dynamic::Value::unnamed_variant( + "Ed25519", + vec![subxt::dynamic::Value::from_bytes(s.encode())], + ), + sp_runtime::MultiSignature::Ecdsa(s) => subxt::dynamic::Value::unnamed_variant( + "Ecdsa", + vec![subxt::dynamic::Value::from_bytes(s.encode())], + ), + sp_runtime::MultiSignature::Eth(s) => subxt::dynamic::Value::unnamed_variant( + "Eth", + vec![subxt::dynamic::Value::from_bytes(s.encode())], + ), + }; - /// Create a request_agreement extrinsic payload (replica agreements). - #[allow(clippy::too_many_arguments)] - pub fn request_agreement( - bucket_id: u64, - provider: AccountId32, - max_bytes: u64, - duration: u32, - payment: u128, - sync_balance: u128, - min_sync_interval: u32, - ) -> impl Payload { subxt::dynamic::tx( PALLET_NAME, - "request_agreement", + "establish_storage_agreement", vec![ - subxt::dynamic::Value::u128(bucket_id as u128), subxt::dynamic::Value::from_bytes(provider.as_ref() as &[u8]), - subxt::dynamic::Value::u128(max_bytes as u128), - subxt::dynamic::Value::u128(duration as u128), - subxt::dynamic::Value::u128(payment), - // ReplicaRequestParams struct - subxt::dynamic::Value::named_composite([ - ("sync_balance", subxt::dynamic::Value::u128(sync_balance)), - ( - "min_sync_interval", - subxt::dynamic::Value::u128(min_sync_interval as u128), - ), - ]), + terms_value, + sig_value, ], ) } diff --git a/client/tests/admin_integration.rs b/client/tests/admin_integration.rs index e7daba94..5e5538b3 100644 --- a/client/tests/admin_integration.rs +++ b/client/tests/admin_integration.rs @@ -18,7 +18,7 @@ use storage_primitives::Role; /// Full bucket management lifecycle in dependency order: /// -/// create_bucket +/// chain_setup (registers Alice as provider + signs terms + opens bucket) /// → get_bucket_info (verify initial state) /// → add_member (add Bob as Writer) /// → get_bucket_info (verify Bob present) @@ -33,6 +33,16 @@ use storage_primitives::Role; async fn test_bucket_lifecycle() { let _guard = chain_guard().await; + // `chain_setup` registers Alice as a provider, signs primary terms with + // her keypair, and redeems them via `establish_storage_agreement` to + // open a fresh bucket. Returns `None` when the chain isn't reachable. + let setup = match common::chain_setup().await { + Some(s) => s, + None => { + eprintln!("Chain not reachable — skipping test_bucket_lifecycle"); + return; + } + }; let admin = match alice_admin().await { Some(c) => c, None => { @@ -42,12 +52,7 @@ async fn test_bucket_lifecycle() { }; let bob_ss58 = dev_ss58("bob"); - - // ── create ──────────────────────────────────────────────────────────────── - let bucket_id = admin - .create_bucket(1) - .await - .expect("create_bucket should succeed"); + let bucket_id = setup.bucket_id; assert!(bucket_id > 0, "bucket_id should be nonzero"); diff --git a/client/tests/common/mod.rs b/client/tests/common/mod.rs index b433de45..19e584ec 100644 --- a/client/tests/common/mod.rs +++ b/client/tests/common/mod.rs @@ -4,9 +4,10 @@ use sp_core::crypto::Ss58Codec; use sp_runtime::AccountId32; use std::sync::{Arc, OnceLock}; use storage_client::{ - AdminClient, ChallengerClient, ClientConfig, DiscoveryClient, ProviderClient, ProviderSettings, - StorageUserClient, + sign_terms, AdminClient, AgreementTermsOf, ChallengerClient, ClientConfig, DiscoveryClient, + ProviderClient, ProviderSettings, StorageUserClient, }; +use storage_primitives::AgreementTerms; use storage_provider_node::{create_router, ProviderState, Storage}; use tokio::net::TcpListener; use tokio::sync::{Mutex, MutexGuard}; @@ -120,19 +121,24 @@ pub async fn chain_setup() -> Option { Ok(Some(_)) ); + let alice_keypair = subxt_signer::sr25519::dev::alice(); + let alice_pubkey = alice_keypair.public_key().0.to_vec(); + if !already_registered { // Idempotent: ignore "already registered" errors so tests survive races. + // We register with Alice's real sr25519 pubkey so she can sign her own + // terms below for the establish_storage_agreement flow. let _ = provider .register( "/ip4/127.0.0.1/tcp/3333".to_string(), - vec![0u8; 32], + alice_pubkey, MIN_STAKE, ) .await; let _ = provider .update_settings(ProviderSettings { - price_per_byte: 1_000_000, + price_per_byte: 0, min_duration: 1, max_duration: 1_000_000, accepting_primary: true, @@ -143,13 +149,35 @@ pub async fn chain_setup() -> Option { .await; } - // ── Create a fresh bucket as Alice ──────────────────────────────────────── + // ── Create a fresh bucket as Alice via signed-terms flow ────────────────── + // Alice is both the provider (signs terms) and the bucket owner (redeems). + // Nonces must be unique per provider, so we pick one based on the current + // block-ish counter — using time-since-epoch nanos so reruns don't collide. let mut admin = AdminClient::new(chain_config(), alice_ss58.clone()).ok()?; if admin.connect().await.is_err() { return None; } admin.set_dev_signer("alice").ok()?; - let bucket_id = admin.create_bucket(1).await.ok()?; + + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + + let terms: AgreementTermsOf = AgreementTerms { + owner: dev_account("alice"), + max_bytes: 1_000_000, + duration: 100, + price_per_byte: 0, + valid_until: u32::MAX, + nonce, + replica_params: None, + }; + let sig = sign_terms(&alice_keypair, &terms); + let bucket_id = admin + .establish_storage_agreement(alice_ss58.clone(), terms, sig) + .await + .ok()?; Some(ChainSetup { alice_ss58, diff --git a/client/tests/provider_integration.rs b/client/tests/provider_integration.rs index 5a51d6e2..96506d91 100644 --- a/client/tests/provider_integration.rs +++ b/client/tests/provider_integration.rs @@ -93,41 +93,6 @@ async fn test_get_provider_info_unregistered_returns_none() { assert!(info.is_none(), "Bob should not be a registered provider"); } -/// `list_pending_requests` should succeed and return a (possibly empty) list of -/// pending agreement requests targeting Alice. -#[tokio::test] -async fn test_list_pending_requests() { - let _guard = chain_guard().await; - - if chain_setup().await.is_none() { - eprintln!("Chain not reachable — skipping test_list_pending_requests"); - return; - } - - let provider = alice_provider() - .await - .expect("chain was already reachable in chain_setup"); - - let requests = provider - .list_pending_requests() - .await - .expect("list_pending_requests should not error"); - - println!("Alice has {} pending request(s)", requests.len()); - - for r in &requests { - assert!( - !r.requester.is_empty(), - "requester field should not be empty" - ); - assert!(r.bucket_id > 0, "bucket_id should be non-zero"); - println!( - " bucket={} requester={} max_bytes={} payment_locked={} duration={}", - r.bucket_id, r.requester, r.max_bytes, r.payment_locked, r.duration - ); - } -} - /// `list_active_agreements` should succeed and return a (possibly empty) list. #[tokio::test] async fn test_list_active_agreements() { @@ -450,29 +415,3 @@ async fn test_add_stake_increases_stake() { ); } -/// `accept_agreement` must fail when no pending request exists for the bucket — -/// the chain has nothing for the provider to accept. -#[tokio::test] -async fn test_accept_agreement_without_request_errors() { - let _guard = chain_guard().await; - - let setup = match chain_setup().await { - Some(s) => s, - None => { - eprintln!( - "Chain not reachable — skipping test_accept_agreement_without_request_errors" - ); - return; - } - }; - - let provider = alice_provider() - .await - .expect("chain was already reachable in chain_setup"); - - let result = provider.accept_agreement(setup.bucket_id).await; - assert!( - result.is_err(), - "accept_agreement should fail when no pending request exists for the bucket" - ); -} From 430f3eae4ff4934d9f18cab8604f634416d76f0a Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 29 May 2026 12:01:24 +0700 Subject: [PATCH 11/44] chore: update runtime weights --- runtime/src/weights/pallet_drive_registry.rs | 77 +++-- runtime/src/weights/pallet_s3_registry.rs | 86 ++--- .../src/weights/pallet_storage_provider.rs | 312 +++++++----------- .../src/weights/pallet_drive_registry.rs | 43 ++- .../src/weights/pallet_s3_registry.rs | 52 ++- .../src/weights/pallet_storage_provider.rs | 136 ++++---- 6 files changed, 297 insertions(+), 409 deletions(-) diff --git a/runtime/src/weights/pallet_drive_registry.rs b/runtime/src/weights/pallet_drive_registry.rs index 1c888c96..41a3f8a4 100644 --- a/runtime/src/weights/pallet_drive_registry.rs +++ b/runtime/src/weights/pallet_drive_registry.rs @@ -46,98 +46,95 @@ use frame_support::{traits::Get, weights::Weight}; use core::marker::PhantomData; +use frame_support::weights::constants::RocksDbWeight; /// Weight functions for `pallet_drive_registry`. pub struct WeightInfo(PhantomData); impl pallet_drive_registry::WeightInfo for WeightInfo { - /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) + /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:5 w:5) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::NextDriveId` (r:1 w:1) /// Proof: `DriveRegistry::NextDriveId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::Drives` (r:0 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `2356` - // Estimated: `18000` - // Minimum execution time: 112_000_000 picoseconds. - Weight::from_parts(122_000_000, 0) - .saturating_add(Weight::from_parts(0, 18000)) - .saturating_add(T::DbWeight::get().reads(16)) - .saturating_add(T::DbWeight::get().writes(13)) + // Measured: `1301` + // Estimated: `11515` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(64_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:1) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:6 w:5) + /// Storage: `StorageProvider::StorageAgreements` (r:2 w:1) /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:6 w:6) + /// Storage: `System::Account` (r:2 w:2) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:5 w:5) + /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:100 w:100) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:0) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) /// Storage: `DriveRegistry::BucketToDrive` (r:0 w:1) /// Proof: `DriveRegistry::BucketToDrive` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) fn delete_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `14785` + // Measured: `12052` // Estimated: `1053490` - // Minimum execution time: 454_000_000 picoseconds. - Weight::from_parts(490_000_000, 0) - .saturating_add(Weight::from_parts(0, 1053490)) - .saturating_add(T::DbWeight::get().reads(121)) - .saturating_add(T::DbWeight::get().writes(120)) + // Minimum execution time: 331_000_000 picoseconds. + Weight::from_parts(367_000_000, 1053490) + .saturating_add(T::DbWeight::get().reads(108_u64)) + .saturating_add(T::DbWeight::get().writes(108_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn share_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4813` + // Measured: `4757` // Estimated: `11515` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(21_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) - /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(231), added: 2706, mode: `MaxEncodedLen`) + /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn unshare_drive() -> Weight { // Proof Size summary in bytes: - // Measured: `4840` + // Measured: `4784` // Estimated: `11515` // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(2)) + Weight::from_parts(22_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } } diff --git a/runtime/src/weights/pallet_s3_registry.rs b/runtime/src/weights/pallet_s3_registry.rs index 461afb37..f3f266cb 100644 --- a/runtime/src/weights/pallet_s3_registry.rs +++ b/runtime/src/weights/pallet_s3_registry.rs @@ -46,6 +46,7 @@ use frame_support::{traits::Get, weights::Weight}; use core::marker::PhantomData; +use frame_support::weights::constants::RocksDbWeight; /// Weight functions for `pallet_s3_registry`. pub struct WeightInfo(PhantomData); @@ -54,6 +55,12 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) /// Storage: `S3Registry::UserBuckets` (r:1 w:1) /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Providers` (r:1 w:1) + /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) @@ -64,15 +71,16 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Buckets` (r:0 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn create_s3_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `1107` + // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(7)) + // Minimum execution time: 59_000_000 picoseconds. + Weight::from_parts(67_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -84,11 +92,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1169` // Estimated: `4315` - // Minimum execution time: 10_000_000 picoseconds. - Weight::from_parts(11_000_000, 0) - .saturating_add(Weight::from_parts(0, 4315)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(20_000_000, 4315) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -98,11 +105,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 26_000_000 picoseconds. - Weight::from_parts(29_000_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(54_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -112,11 +118,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(40_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:2 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -126,40 +131,9 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8205` // Estimated: `21362` - // Minimum execution time: 28_000_000 picoseconds. - Weight::from_parts(31_000_000, 0) - .saturating_add(Weight::from_parts(0, 21362)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) - } - /// Storage: `S3Registry::BucketNameToId` (r:1 w:1) - /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::UserBuckets` (r:1 w:1) - /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::NextS3BucketId` (r:1 w:1) - /// Proof: `S3Registry::NextS3BucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::S3Buckets` (r:0 w:1) - /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_s3_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `2356` - // Estimated: `18000` - // Minimum execution time: 51_000_000 picoseconds. - Weight::from_parts(56_000_000, 0) - .saturating_add(Weight::from_parts(0, 18000)) - .saturating_add(T::DbWeight::get().reads(13)) - .saturating_add(T::DbWeight::get().writes(9)) + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(58_000_000, 21362) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } } diff --git a/runtime/src/weights/pallet_storage_provider.rs b/runtime/src/weights/pallet_storage_provider.rs index bf2decaf..054096f8 100644 --- a/runtime/src/weights/pallet_storage_provider.rs +++ b/runtime/src/weights/pallet_storage_provider.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `192.168.0.104`, CPU: `` +//! HOSTNAME: `192.168.0.105`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -56,10 +56,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn register_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `183` + // Measured: `107` // Estimated: `3825` // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(15_000_000, 0) + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -68,7 +68,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn deregister_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `345` + // Measured: `272` // Estimated: `3825` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 0) @@ -84,10 +84,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn complete_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `45127` + // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 3_490_000_000 picoseconds. - Weight::from_parts(3_661_000_000, 0) + // Minimum execution time: 3_734_000_000 picoseconds. + Weight::from_parts(3_838_000_000, 0) .saturating_add(Weight::from_parts(0, 2566553)) .saturating_add(T::DbWeight::get().reads(1003)) .saturating_add(T::DbWeight::get().writes(1002)) @@ -96,7 +96,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn cancel_deregister() -> Weight { // Proof Size summary in bytes: - // Measured: `328` + // Measured: `255` // Estimated: `3825` // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(6_000_000, 0) @@ -108,7 +108,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_settings() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(6_000_000, 0) @@ -122,10 +122,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn add_stake() -> Weight { // Proof Size summary in bytes: - // Measured: `427` + // Measured: `354` // Estimated: `3825` // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(14_000_000, 0) + Weight::from_parts(15_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -136,10 +136,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn block_extensions() -> Weight { // Proof Size summary in bytes: - // Measured: `362` + // Measured: `395` // Estimated: `3825` - // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 0) + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) @@ -148,7 +148,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn update_provider_multiaddr() -> Weight { // Proof Size summary in bytes: - // Measured: `324` + // Measured: `251` // Estimated: `3825` // Minimum execution time: 5_000_000 picoseconds. Weight::from_parts(6_000_000, 0) @@ -156,53 +156,15 @@ impl pallet_storage_provider::WeightInfo for WeightInfo .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_bucket() -> Weight { - // Proof Size summary in bytes: - // Measured: `160` - // Estimated: `11515` - // Minimum execution time: 7_000_000 picoseconds. - Weight::from_parts(8_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: `StorageProvider::Providers` (r:2 w:1) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn create_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `505` - // Estimated: `11515` - // Minimum execution time: 25_000_000 picoseconds. - Weight::from_parts(27_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(6)) - } /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_bucket_min_providers() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` + // Measured: `358` + // Estimated: `3823` // Minimum execution time: 4_000_000 picoseconds. Weight::from_parts(5_000_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -210,11 +172,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `581` - // Estimated: `4046` + // Measured: `543` + // Estimated: `4008` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 0) - .saturating_add(Weight::from_parts(0, 4046)) + .saturating_add(Weight::from_parts(0, 4008)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -224,10 +186,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn set_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `507` + // Measured: `427` // Estimated: `11515` // Minimum execution time: 8_000_000 picoseconds. - Weight::from_parts(9_000_000, 0) + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -238,7 +200,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) fn remove_bucket_member() -> Weight { // Proof Size summary in bytes: - // Measured: `602` + // Measured: `497` // Estimated: `11515` // Minimum execution time: 9_000_000 picoseconds. Weight::from_parts(10_000_000, 0) @@ -256,95 +218,57 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `985` - // Estimated: `4450` - // Minimum execution time: 21_000_000 picoseconds. - Weight::from_parts(23_000_000, 0) - .saturating_add(Weight::from_parts(0, 4450)) + // Measured: `947` + // Estimated: `4412` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 0) + .saturating_add(Weight::from_parts(0, 4412)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } - /// Storage: `StorageProvider::Buckets` (r:1 w:0) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) + /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayStates` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayStates` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_agreement() -> Weight { + /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) + /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) + /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::Buckets` (r:0 w:1) + /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) + fn establish_storage_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `612` - // Estimated: `4077` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) - .saturating_add(Weight::from_parts(0, 4077)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `354` + // Estimated: `11515` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(48_000_000, 0) + .saturating_add(Weight::from_parts(0, 11515)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `StorageProvider::Buckets` (r:1 w:0) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_primary_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `774` - // Estimated: `4239` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) - .saturating_add(Weight::from_parts(0, 4239)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) - } + /// Storage: `StorageProvider::StorageAgreements` (r:1 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:1 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn accept_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `833` - // Estimated: `4298` - // Minimum execution time: 16_000_000 picoseconds. - Weight::from_parts(17_000_000, 0) - .saturating_add(Weight::from_parts(0, 4298)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn reject_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(15_000_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayStates` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayStates` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn withdraw_agreement_request() -> Weight { + fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_000_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `734` + // Estimated: `4199` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(47_000_000, 0) + .saturating_add(Weight::from_parts(0, 4199)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -354,10 +278,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn top_up_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `606` + // Measured: `639` // Estimated: `3825` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 0) + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) @@ -370,10 +294,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn extend_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `709` + // Measured: `742` // Estimated: `6196` - // Minimum execution time: 44_000_000 picoseconds. - Weight::from_parts(48_000_000, 0) + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) @@ -389,13 +313,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `a` is `[0, 1]`. fn end_agreement(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1088 + a * (103 ±0)` + // Measured: `1050 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(45_483_673, 0) + Weight::from_parts(46_659_183, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 147_900 - .saturating_add(Weight::from_parts(20_091_326, 0).saturating_mul(a.into())) + // Standard Error: 151_274 + .saturating_add(Weight::from_parts(22_140_816, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5)) @@ -412,9 +336,9 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_expired_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `1088` + // Measured: `1050` // Estimated: `6196` - // Minimum execution time: 41_000_000 picoseconds. + // Minimum execution time: 42_000_000 picoseconds. Weight::from_parts(45_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) @@ -426,10 +350,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1768` + // Measured: `1697` // Estimated: `15165` - // Minimum execution time: 121_000_000 picoseconds. - Weight::from_parts(125_000_000, 0) + // Minimum execution time: 122_000_000 picoseconds. + Weight::from_parts(130_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -440,10 +364,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn extend_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `1822` + // Measured: `1751` // Estimated: `15165` - // Minimum execution time: 121_000_000 picoseconds. - Weight::from_parts(123_000_000, 0) + // Minimum execution time: 122_000_000 picoseconds. + Weight::from_parts(130_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -456,11 +380,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointPool` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) fn fund_checkpoint_pool() -> Weight { // Proof Size summary in bytes: - // Measured: `324` - // Estimated: `3789` + // Measured: `253` + // Estimated: `3718` // Minimum execution time: 15_000_000 picoseconds. Weight::from_parts(16_000_000, 0) - .saturating_add(Weight::from_parts(0, 3789)) + .saturating_add(Weight::from_parts(0, 3718)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -479,13 +403,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `s` is `[1, 5]`. fn provider_checkpoint(s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `564 + s * (258 ±0)` - // Estimated: `4029 + s * (2835 ±0)` + // Measured: `493 + s * (258 ±0)` + // Estimated: `3958 + s * (2835 ±0)` // Minimum execution time: 38_000_000 picoseconds. - Weight::from_parts(16_685_922, 0) - .saturating_add(Weight::from_parts(0, 4029)) - // Standard Error: 65_269 - .saturating_add(Weight::from_parts(23_495_268, 0).saturating_mul(s.into())) + Weight::from_parts(16_842_932, 0) + .saturating_add(Weight::from_parts(0, 3958)) + // Standard Error: 27_287 + .saturating_add(Weight::from_parts(24_466_296, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4)) @@ -497,11 +421,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointConfigs` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) fn configure_checkpoint_window() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` - // Minimum execution time: 6_000_000 picoseconds. - Weight::from_parts(7_000_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + // Measured: `358` + // Estimated: `3823` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -517,7 +441,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn report_missed_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `967` + // Measured: `929` // Estimated: `6196` // Minimum execution time: 30_000_000 picoseconds. Weight::from_parts(33_000_000, 0) @@ -531,9 +455,9 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn claim_checkpoint_rewards() -> Weight { // Proof Size summary in bytes: - // Measured: `364` + // Measured: `397` // Estimated: `3593` - // Minimum execution time: 14_000_000 picoseconds. + // Minimum execution time: 15_000_000 picoseconds. Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(2)) @@ -549,11 +473,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `893` - // Estimated: `4358` + // Measured: `855` + // Estimated: `4320` // Minimum execution time: 17_000_000 picoseconds. Weight::from_parts(19_000_000, 0) - .saturating_add(Weight::from_parts(0, 4358)) + .saturating_add(Weight::from_parts(0, 4320)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -569,11 +493,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `667` - // Estimated: `4132` + // Measured: `629` + // Estimated: `4094` // Minimum execution time: 41_000_000 picoseconds. Weight::from_parts(44_000_000, 0) - .saturating_add(Weight::from_parts(0, 4132)) + .saturating_add(Weight::from_parts(0, 4094)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -587,11 +511,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `755` - // Estimated: `4220` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) - .saturating_add(Weight::from_parts(0, 4220)) + // Measured: `788` + // Estimated: `4253` + // Minimum execution time: 19_000_000 picoseconds. + Weight::from_parts(21_000_000, 0) + .saturating_add(Weight::from_parts(0, 4253)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -605,10 +529,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `1131` + // Measured: `1093` // Estimated: `6196` - // Minimum execution time: 407_000_000 picoseconds. - Weight::from_parts(434_000_000, 0) + // Minimum execution time: 409_000_000 picoseconds. + Weight::from_parts(436_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -623,10 +547,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn respond_to_challenge_deleted() -> Weight { // Proof Size summary in bytes: - // Measured: `1344` + // Measured: `1306` // Estimated: `6660` // Minimum execution time: 53_000_000 picoseconds. - Weight::from_parts(57_000_000, 0) + Weight::from_parts(58_000_000, 0) .saturating_add(Weight::from_parts(0, 6660)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -641,7 +565,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_superseded() -> Weight { // Proof Size summary in bytes: - // Measured: `1185` + // Measured: `1147` // Estimated: `6196` // Minimum execution time: 28_000_000 picoseconds. Weight::from_parts(31_000_000, 0) @@ -657,10 +581,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `1007` + // Measured: `969` // Estimated: `6196` - // Minimum execution time: 37_000_000 picoseconds. - Weight::from_parts(40_000_000, 0) + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(42_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) @@ -671,10 +595,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn top_up_replica_sync_balance() -> Weight { // Proof Size summary in bytes: - // Measured: `473` + // Measured: `506` // Estimated: `3692` // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(15_000_000, 0) + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3692)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs b/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs index 58de557a..41a3f8a4 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs @@ -17,7 +17,7 @@ //! Autogenerated weights for `pallet_drive_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -28,10 +28,10 @@ // benchmark // pallet // --extrinsic=* -// --runtime=target/production/wbuild/storage-paseo-runtime/storage_paseo_runtime.wasm +// --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_drive_registry // --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt -// --output=./runtimes/web3-storage-paseo/src/weights +// --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 // --repeat=20 @@ -46,11 +46,12 @@ use frame_support::{traits::Get, weights::Weight}; use core::marker::PhantomData; +use frame_support::weights::constants::RocksDbWeight; /// Weight functions for `pallet_drive_registry`. pub struct WeightInfo(PhantomData); impl pallet_drive_registry::WeightInfo for WeightInfo { - /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) + /// Storage: `DriveRegistry::UserDrives` (r:1 w:1) /// Proof: `DriveRegistry::UserDrives` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -76,11 +77,10 @@ impl pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 @@ -28,10 +28,10 @@ // benchmark // pallet // --extrinsic=* -// --runtime=target/production/wbuild/storage-paseo-runtime/storage_paseo_runtime.wasm +// --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_s3_registry // --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt -// --output=./runtimes/web3-storage-paseo/src/weights +// --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 // --repeat=20 @@ -46,6 +46,7 @@ use frame_support::{traits::Get, weights::Weight}; use core::marker::PhantomData; +use frame_support::weights::constants::RocksDbWeight; /// Weight functions for `pallet_s3_registry`. pub struct WeightInfo(PhantomData); @@ -76,11 +77,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 55_000_000 picoseconds. - Weight::from_parts(59_000_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(8)) - .saturating_add(T::DbWeight::get().writes(11)) + // Minimum execution time: 59_000_000 picoseconds. + Weight::from_parts(67_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -92,11 +92,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1169` // Estimated: `4315` - // Minimum execution time: 10_000_000 picoseconds. - Weight::from_parts(11_000_000, 0) - .saturating_add(Weight::from_parts(0, 4315)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(20_000_000, 4315) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -106,11 +105,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 26_000_000 picoseconds. - Weight::from_parts(28_000_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(54_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -120,11 +118,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(40_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:2 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -134,10 +131,9 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8205` // Estimated: `21362` - // Minimum execution time: 28_000_000 picoseconds. - Weight::from_parts(31_000_000, 0) - .saturating_add(Weight::from_parts(0, 21362)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(58_000_000, 21362) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } } diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs b/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs index b9f1f976..1acb3865 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-05-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `192.168.0.104`, CPU: `` +//! HOSTNAME: `192.168.0.105`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -86,8 +86,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 3_686_000_000 picoseconds. - Weight::from_parts(3_821_000_000, 0) + // Minimum execution time: 3_521_000_000 picoseconds. + Weight::from_parts(3_656_000_000, 0) .saturating_add(Weight::from_parts(0, 2566553)) .saturating_add(T::DbWeight::get().reads(1003)) .saturating_add(T::DbWeight::get().writes(1002)) @@ -125,7 +125,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `354` // Estimated: `3825` // Minimum execution time: 13_000_000 picoseconds. - Weight::from_parts(15_000_000, 0) + Weight::from_parts(14_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -136,7 +136,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn block_extensions() -> Weight { // Proof Size summary in bytes: - // Measured: `399` + // Measured: `395` // Estimated: `3825` // Minimum execution time: 8_000_000 picoseconds. Weight::from_parts(9_000_000, 0) @@ -172,11 +172,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `510` - // Estimated: `3975` + // Measured: `543` + // Estimated: `4008` // Minimum execution time: 6_000_000 picoseconds. Weight::from_parts(7_000_000, 0) - .saturating_add(Weight::from_parts(0, 3975)) + .saturating_add(Weight::from_parts(0, 4008)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -218,18 +218,18 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `951` - // Estimated: `4416` + // Measured: `947` + // Estimated: `4412` // Minimum execution time: 22_000_000 picoseconds. Weight::from_parts(24_000_000, 0) - .saturating_add(Weight::from_parts(0, 4416)) + .saturating_add(Weight::from_parts(0, 4412)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) - /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayStates` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayStates` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) @@ -245,7 +245,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `354` // Estimated: `11515` // Minimum execution time: 44_000_000 picoseconds. - Weight::from_parts(48_000_000, 0) + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(7)) @@ -256,17 +256,17 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::ProviderReplayState` (r:1 w:1) - /// Proof: `StorageProvider::ProviderReplayState` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayStates` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayStates` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `737` - // Estimated: `4202` - // Minimum execution time: 44_000_000 picoseconds. - Weight::from_parts(47_000_000, 0) - .saturating_add(Weight::from_parts(0, 4202)) + // Measured: `734` + // Estimated: `4199` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(44_000_000, 0) + .saturating_add(Weight::from_parts(0, 4199)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -278,10 +278,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn top_up_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `643` + // Measured: `639` // Estimated: `3825` // Minimum execution time: 18_000_000 picoseconds. - Weight::from_parts(20_000_000, 0) + Weight::from_parts(19_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) @@ -294,10 +294,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn extend_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `746` + // Measured: `742` // Estimated: `6196` - // Minimum execution time: 45_000_000 picoseconds. - Weight::from_parts(49_000_000, 0) + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(47_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) @@ -313,13 +313,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `a` is `[0, 1]`. fn end_agreement(a: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `1054 + a * (103 ±0)` + // Measured: `1050 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` - // Minimum execution time: 43_000_000 picoseconds. - Weight::from_parts(46_987_755, 0) + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(44_591_836, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 138_728 - .saturating_add(Weight::from_parts(22_012_244, 0).saturating_mul(a.into())) + // Standard Error: 258_954 + .saturating_add(Weight::from_parts(20_508_163, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5)) @@ -336,10 +336,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn claim_expired_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `1054` + // Measured: `1050` // Estimated: `6196` - // Minimum execution time: 42_000_000 picoseconds. - Weight::from_parts(46_000_000, 0) + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(44_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(5)) @@ -353,7 +353,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `1697` // Estimated: `15165` // Minimum execution time: 121_000_000 picoseconds. - Weight::from_parts(130_000_000, 0) + Weight::from_parts(124_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -366,8 +366,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1751` // Estimated: `15165` - // Minimum execution time: 122_000_000 picoseconds. - Weight::from_parts(132_000_000, 0) + // Minimum execution time: 121_000_000 picoseconds. + Weight::from_parts(125_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -382,7 +382,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `253` // Estimated: `3718` - // Minimum execution time: 15_000_000 picoseconds. + // Minimum execution time: 14_000_000 picoseconds. Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3718)) .saturating_add(T::DbWeight::get().reads(3)) @@ -406,10 +406,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Measured: `493 + s * (258 ±0)` // Estimated: `3958 + s * (2835 ±0)` // Minimum execution time: 38_000_000 picoseconds. - Weight::from_parts(16_923_714, 0) + Weight::from_parts(7_075_759, 0) .saturating_add(Weight::from_parts(0, 3958)) - // Standard Error: 31_634 - .saturating_add(Weight::from_parts(24_196_144, 0).saturating_mul(s.into())) + // Standard Error: 361_690 + .saturating_add(Weight::from_parts(30_789_778, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4)) @@ -441,10 +441,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn report_missed_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `896` + // Measured: `929` // Estimated: `6196` - // Minimum execution time: 30_000_000 picoseconds. - Weight::from_parts(33_000_000, 0) + // Minimum execution time: 33_000_000 picoseconds. + Weight::from_parts(35_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -455,10 +455,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn claim_checkpoint_rewards() -> Weight { // Proof Size summary in bytes: - // Measured: `364` + // Measured: `397` // Estimated: `3593` - // Minimum execution time: 14_000_000 picoseconds. - Weight::from_parts(16_000_000, 0) + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(21_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -473,11 +473,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `822` - // Estimated: `4287` - // Minimum execution time: 17_000_000 picoseconds. - Weight::from_parts(19_000_000, 0) - .saturating_add(Weight::from_parts(0, 4287)) + // Measured: `855` + // Estimated: `4320` + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) + .saturating_add(Weight::from_parts(0, 4320)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -493,11 +493,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `633` - // Estimated: `4098` + // Measured: `629` + // Estimated: `4094` // Minimum execution time: 41_000_000 picoseconds. Weight::from_parts(44_000_000, 0) - .saturating_add(Weight::from_parts(0, 4098)) + .saturating_add(Weight::from_parts(0, 4094)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -511,11 +511,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `792` - // Estimated: `4257` - // Minimum execution time: 19_000_000 picoseconds. - Weight::from_parts(21_000_000, 0) - .saturating_add(Weight::from_parts(0, 4257)) + // Measured: `788` + // Estimated: `4253` + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) + .saturating_add(Weight::from_parts(0, 4253)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -529,10 +529,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_proof() -> Weight { // Proof Size summary in bytes: - // Measured: `1060` + // Measured: `1093` // Estimated: `6196` - // Minimum execution time: 406_000_000 picoseconds. - Weight::from_parts(433_000_000, 0) + // Minimum execution time: 407_000_000 picoseconds. + Weight::from_parts(434_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -547,7 +547,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn respond_to_challenge_deleted() -> Weight { // Proof Size summary in bytes: - // Measured: `1273` + // Measured: `1306` // Estimated: `6660` // Minimum execution time: 53_000_000 picoseconds. Weight::from_parts(57_000_000, 0) @@ -565,7 +565,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn respond_to_challenge_superseded() -> Weight { // Proof Size summary in bytes: - // Measured: `1114` + // Measured: `1147` // Estimated: `6196` // Minimum execution time: 28_000_000 picoseconds. Weight::from_parts(31_000_000, 0) @@ -581,7 +581,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `973` + // Measured: `969` // Estimated: `6196` // Minimum execution time: 38_000_000 picoseconds. Weight::from_parts(42_000_000, 0) @@ -595,7 +595,7 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn top_up_replica_sync_balance() -> Weight { // Proof Size summary in bytes: - // Measured: `510` + // Measured: `506` // Estimated: `3692` // Minimum execution time: 15_000_000 picoseconds. Weight::from_parts(16_000_000, 0) From 50f239460c805ea585b22373ef0dacfe6541e08e Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 29 May 2026 15:46:12 +0700 Subject: [PATCH 12/44] feat: add negotiate endpoint and persistent nonce counter to provider-node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add provider-node/src/negotiate.rs: - NonceCounter: atomic monotonic counter persisted to disk on every allocation, can continue with on-chain hwm. - sign_terms() mirrors the on-chain verifier: blake2_256(SCALE(terms)) → sr25519 sign → MultiSignature::Sr25519. - Wire POST negotiate in api.rs: allocates the next nonce, builds AgreementTerms, signs, then returns SignedTerms (error 503 if the node has no signing key). - command.rs: drop start_agreement_coordinator; replace with setup_nonce_counter. - Delete provider-node/src/agreement_coordinator.rs. --- Cargo.lock | 2 + client/examples/complete_workflow.rs | 221 ------------ client/src/admin.rs | 5 +- client/src/agreement.rs | 38 +- client/src/lib.rs | 10 +- client/src/provider.rs | 62 ++-- client/src/substrate.rs | 33 +- client/tests/provider_integration.rs | 1 - pallet/src/benchmarking.rs | 8 +- pallet/src/lib.rs | 22 +- provider-node/Cargo.toml | 2 + provider-node/src/agreement_coordinator.rs | 333 ------------------ provider-node/src/api.rs | 35 ++ provider-node/src/cli.rs | 28 -- provider-node/src/command.rs | 89 +++-- provider-node/src/lib.rs | 11 +- provider-node/src/negotiate.rs | 159 +++++++++ provider-node/tests/complete_workflow.rs | 186 ++++++++++ runtimes/web3-storage-paseo/tests/tests.rs | 16 +- .../file-system/pallet-registry/src/lib.rs | 3 +- .../file-system/pallet-registry/src/tests.rs | 22 +- .../file-system/primitives/src/lib.rs | 2 +- .../s3/pallet-s3-registry/src/benchmarking.rs | 4 +- .../s3/pallet-s3-registry/src/lib.rs | 1 - 24 files changed, 563 insertions(+), 730 deletions(-) delete mode 100644 client/examples/complete_workflow.rs delete mode 100644 provider-node/src/agreement_coordinator.rs create mode 100644 provider-node/src/negotiate.rs create mode 100644 provider-node/tests/complete_workflow.rs diff --git a/Cargo.lock b/Cargo.lock index e708a07c..eb9ea386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8014,6 +8014,8 @@ dependencies = [ "serde", "serde_json", "sp-core", + "sp-runtime", + "storage-client", "storage-primitives", "subxt", "subxt-signer", diff --git a/client/examples/complete_workflow.rs b/client/examples/complete_workflow.rs deleted file mode 100644 index 7342304e..00000000 --- a/client/examples/complete_workflow.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! Complete workflow example demonstrating all client types. -//! -//! This example shows: -//! 1. Provider registration -//! 2. Negotiating provider-signed terms over HTTP -//! 3. Redeeming the signed terms on-chain to open a bucket + agreement -//! 4. Data upload by storage user -//! 5. Checkpoint creation -//! 6. Challenge by third party -//! 7. Challenge response by provider - -use storage_client::{ - AdminClient, ChallengerClient, ChunkingStrategy, ClientConfig, NegotiateRequest, - ProviderClient, StorageUserClient, -}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize tracing for logs - tracing_subscriber::fmt::init(); - - println!("=== Scalable Web3 Storage Complete Workflow ===\n"); - - // Configuration - let config = ClientConfig { - chain_ws_url: "ws://localhost:2222".to_string(), - provider_urls: vec!["http://localhost:3333".to_string()], - timeout_secs: 30, - enable_retries: true, - }; - - // ═════════════════════════════════════════════════════════════════════════ - // Step 1: Provider Registration - // ═════════════════════════════════════════════════════════════════════════ - println!("📦 Step 1: Provider Registration"); - - let provider_account = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"; - let provider_client = ProviderClient::new(config.clone(), provider_account.to_string())?; - - println!(" Registering provider {provider_account}..."); - provider_client - .register( - "/ip4/203.0.113.1/tcp/3333".to_string(), - vec![0u8; 32], // Mock public key - 10_000_000_000_000, // 10 tokens stake - ) - .await?; - println!(" ✓ Provider registered with 10 tokens stake\n"); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 2: Negotiate signed terms with the provider - // ═════════════════════════════════════════════════════════════════════════ - println!("🤝 Step 2: Negotiating storage terms"); - - let admin_account = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; - let admin_client = AdminClient::new(config.clone(), admin_account.to_string())?; - - let provider_http_url = config.provider_urls[0].clone(); - println!(" Asking provider for signed terms over HTTP..."); - let signed = ProviderClient::negotiate_terms( - &provider_http_url, - &NegotiateRequest { - owner: admin_account.parse()?, - max_bytes: 10 * 1024 * 1024 * 1024, // 10 GB - duration: 100_000, // ~2 weeks at 6 sec blocks - valid_until_offset: 1_000, - }, - ) - .await?; - println!( - " ✓ Provider signed terms: max_bytes={}, duration={}, nonce={}", - signed.terms.max_bytes, signed.terms.duration, signed.terms.nonce - ); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 3: Redeem signed terms on-chain (bucket + agreement atomically) - // ═════════════════════════════════════════════════════════════════════════ - println!("📦 Step 3: Establishing storage agreement on-chain"); - - let bucket_id = admin_client - .establish_storage_agreement( - provider_account.to_string(), - signed.terms, - signed.signature, - ) - .await?; - println!(" ✓ Bucket {bucket_id} created + primary agreement opened\n"); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 4: Data Upload by Storage User - // ═════════════════════════════════════════════════════════════════════════ - println!("⬆️ Step 4: Data Upload"); - - let user_client = StorageUserClient::new(config.clone())?; - - let data = b"Hello, decentralized world! This is my important data that \ - needs to be stored reliably across multiple providers. \ - It's content-addressed and cryptographically verified!"; - - println!(" Uploading {} bytes...", data.len()); - let data_root = user_client - .upload(bucket_id, data, ChunkingStrategy::default()) - .await?; - println!( - " ✓ Data uploaded with root: 0x{}", - hex::encode(data_root.as_bytes()) - ); - - println!(" Committing to chain..."); - let commitment = user_client.commit(bucket_id, vec![data_root]).await?; - println!(" ✓ Committed with MMR root: {}\n", commitment.mmr_root); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 5: Data Verification - // ═════════════════════════════════════════════════════════════════════════ - println!("✅ Step 5: Data Verification"); - - println!(" Downloading data..."); - let retrieved_data = user_client - .download(&data_root, 0, data.len() as u64) - .await?; - println!(" ✓ Downloaded {} bytes", retrieved_data.len()); - - if retrieved_data == data { - println!(" ✓ Data integrity verified!"); - } else { - println!(" ✗ Data mismatch!"); - } - println!(); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 6: Challenge by Third Party - // ═════════════════════════════════════════════════════════════════════════ - println!("🎯 Step 6: Data Integrity Challenge"); - - let challenger_account = "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy"; - let challenger_client = ChallengerClient::new(config.clone(), challenger_account.to_string())?; - - println!(" Challenger analyzing provider..."); - let analysis = challenger_client - .analyze_provider(bucket_id, provider_account.to_string()) - .await?; - println!(" Provider reputation: {}", analysis.reputation); - println!(" Recommendation: {:?}", analysis.recommendation); - - println!(" Creating challenge..."); - let challenge_id = challenger_client - .challenge_checkpoint( - bucket_id, - provider_account.to_string(), - 0, // leaf_index - 0, // chunk_index - ) - .await?; - println!( - " ✓ Challenge created: deadline={}, index={}", - challenge_id.deadline, challenge_id.index - ); - println!(" Provider must respond within challenge timeout!\n"); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 7: Challenge Response by Provider - // ═════════════════════════════════════════════════════════════════════════ - println!("🛡️ Step 7: Provider Response"); - - println!(" Provider fetching challenged data from local storage..."); - // In a real scenario, provider would: - // 1. Load chunk from local storage - // 2. Generate Merkle proof - // 3. Generate MMR proof - // 4. Submit response on-chain - - println!(" ✓ Provider would respond with proofs\n"); - - // ═════════════════════════════════════════════════════════════════════════ - // Step 8: Monitoring & Analytics - // ═════════════════════════════════════════════════════════════════════════ - println!("📊 Step 8: Monitoring & Analytics"); - - println!(" Provider statistics:"); - let provider_stats = provider_client.get_stats().await?; - println!( - " - Total agreements: {}", - provider_stats.agreements_total - ); - println!( - " - Challenges received: {}", - provider_stats.challenges_received - ); - println!(" - Reputation: {}/100", provider_stats.reputation); - - println!("\n Challenger statistics:"); - let challenge_stats = challenger_client.get_challenge_stats().await?; - println!( - " - Total challenges: {}", - challenge_stats.total_challenges - ); - println!( - " - Success rate: {:.1}%", - if challenge_stats.total_challenges > 0 { - (challenge_stats.successful_challenges as f64 / challenge_stats.total_challenges as f64) - * 100.0 - } else { - 0.0 - } - ); - println!( - " - Total earnings: {} tokens", - challenge_stats.total_earnings - ); - - println!("\n=== Workflow Complete ==="); - println!("\n💡 Key Takeaways:"); - println!(" • Providers stake collateral and offer storage"); - println!(" • Admins create buckets and manage agreements"); - println!(" • Users upload data with cryptographic guarantees"); - println!(" • Challengers enforce accountability"); - println!(" • All operations are verifiable on-chain"); - - Ok(()) -} diff --git a/client/src/admin.rs b/client/src/admin.rs index 380ecdb4..450ce4e2 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -63,7 +63,7 @@ impl AdminClient { /// /// # Example /// ```no_run - /// # use storage_client::{AdminClient, ProviderClient, NegotiateRequest}; + /// # use storage_client::{AdminClient, NegotiateRequest, ProviderClient}; /// # async fn example() -> Result<(), Box> { /// let client = AdminClient::with_defaults("5GrwvaEF...".to_string())?; /// let signed = ProviderClient::negotiate_terms( @@ -72,7 +72,8 @@ impl AdminClient { /// owner: "5GrwvaEF...".parse()?, /// max_bytes: 1_000_000, /// duration: 100, - /// valid_until_offset: 1_000, + /// price_per_byte: 1_000_000, + /// replica_params: None, /// }, /// ).await?; /// let bucket_id = client.establish_storage_agreement( diff --git a/client/src/agreement.rs b/client/src/agreement.rs index 277498da..c86e4eab 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -27,34 +27,33 @@ use storage_primitives::AgreementTerms; /// Concrete [`AgreementTerms`] type for the storage parachain. /// /// Balance is `u128`, BlockNumber is `u32`; matches `parachains_common`'s -/// runtime types used by `storage_paseo_runtime`. +/// runtime types used by runtime. pub type AgreementTermsOf = AgreementTerms; -/// Request body that a bucket owner POSTs to a provider node's -/// `/negotiate` endpoint. -/// -/// The provider node turns this into [`AgreementTerms`] (filling in -/// `price_per_byte` from its own settings, choosing a `nonce`, and -/// resolving `valid_until = current_block + valid_until_offset`), signs -/// the SCALE encoding, and returns [`SignedTerms`]. +/// Concrete `ReplicaTerms` matching the parachain's +/// `(Balance, BlockNumber) = (u128, u32)`. +pub type ReplicaTermsOf = storage_primitives::ReplicaTerms; + +/// The owner proposes the agreement shape they want; the provider node +/// allocates a fresh nonce and a validity window from its own state, +/// builds the full [`AgreementTermsOf`], signs it, and returns +/// [`SignedTerms`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NegotiateRequest { - /// The account that will own the resulting bucket / drive. Must - /// match the `Signed` origin that later submits - /// `establish_storage_agreement`. + /// Account that will own the resulting bucket. pub owner: AccountId32, /// Storage quota requested, in bytes. pub max_bytes: u64, /// Agreement duration in blocks from activation. pub duration: u32, - /// How many blocks the resulting quote should be valid for. The - /// provider chooses `valid_until = current_block + valid_until_offset` - /// so the owner has a bounded window to redeem it on-chain. - pub valid_until_offset: u32, + /// Price per byte per block the owner is willing to lock in. + pub price_per_byte: u128, + /// `Some(_)` to negotiate a replica agreement (per-sync funding + + /// minimum sync interval); `None` for a primary agreement. + pub replica_params: Option, } -/// Provider-signed agreement terms ready to be redeemed via -/// `establish_storage_agreement`. +/// Provider-signed agreement terms /// /// `signature` is a `MultiSignature` over `blake2_256(SCALE(terms))`, /// produced by the provider's registered key. We carry the signature as @@ -72,7 +71,10 @@ pub struct SignedTerms { /// Mirror of the on-chain `verify_terms_signature`: SCALE-encode, hash /// with blake2-256, then sign. The runtime accepts `MultiSignature`, so /// callers wrap the raw sr25519 signature with `MultiSignature::Sr25519`. -pub fn sign_terms(keypair: &subxt_signer::sr25519::Keypair, terms: &AgreementTermsOf) -> MultiSignature { +pub fn sign_terms( + keypair: &subxt_signer::sr25519::Keypair, + terms: &AgreementTermsOf, +) -> MultiSignature { let hash = blake2_256(&terms.encode()); let raw = keypair.sign(&hash); MultiSignature::Sr25519(sp_core::sr25519::Signature::from_raw(raw.0)) diff --git a/client/src/lib.rs b/client/src/lib.rs index 02bd5c03..8f822918 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -49,19 +49,21 @@ //! ### For Bucket Administrators //! [`AdminClient`](admin::AdminClient) - Manage buckets and agreements //! ```no_run -//! use storage_client::{AdminClient, ProviderClient, NegotiateRequest}; +//! use storage_client::{AdminClient, NegotiateRequest, ProviderClient}; //! //! # async fn example() -> Result<(), Box> { //! let client = AdminClient::with_defaults("5GrwvaEF...".to_string())?; //! -//! // 1. Negotiate signed terms with the provider over HTTP. +//! // 1. Ask the provider node to sign agreement terms over HTTP. The +//! // provider allocates the nonce + validity window and signs. //! let signed = ProviderClient::negotiate_terms( //! "http://provider.example:3333", //! &NegotiateRequest { //! owner: "5GrwvaEF...".parse()?, //! max_bytes: 10 * 1024 * 1024 * 1024, // 10 GB //! duration: 100_000, -//! valid_until_offset: 1_000, +//! price_per_byte: 1_000_000, +//! replica_params: None, //! }, //! ).await?; //! @@ -113,7 +115,7 @@ pub mod verification; // Re-export commonly used types pub use admin::AdminClient; -pub use agreement::{sign_terms, AgreementTermsOf, NegotiateRequest, SignedTerms}; +pub use agreement::{sign_terms, AgreementTermsOf, NegotiateRequest, ReplicaTermsOf, SignedTerms}; pub use base::{ChunkingStrategy, ClientConfig, ClientError, ClientResult}; pub use challenger::ChallengerClient; pub use checkpoint::{ diff --git a/client/src/provider.rs b/client/src/provider.rs index 0295b052..bbad7562 100644 --- a/client/src/provider.rs +++ b/client/src/provider.rs @@ -310,37 +310,41 @@ impl ProviderClient { // Term Negotiation (off-chain) // ═════════════════════════════════════════════════════════════════════════ + /// Read a provider's on-chain `ProviderReplayState.hwm`. Returns + /// `Ok(None)` if the provider has no replay state yet (never signed + /// any terms). + pub async fn fetch_replay_hwm( + chain_ws_url: &str, + provider: &AccountId32, + ) -> ClientResult> { + let chain = SubstrateClient::connect(chain_ws_url).await?; + let thunk = chain + .api() + .storage() + .at_latest() + .await + .map_err(|e| ClientError::Chain(format!("Failed to get storage: {e}")))? + .fetch(&storage::provider_replay_state(provider)) + .await + .map_err(|e| ClientError::Chain(format!("Failed to fetch replay state: {e}")))?; + + let Some(thunk) = thunk else { + return Ok(None); + }; + let decoded = thunk + .to_value() + .map_err(|e| ClientError::Chain(format!("Failed to decode replay state: {e}")))?; + Ok(named_field(&decoded, "hwm") + .and_then(|v| v.as_u128()) + .map(|h| h as u64)) + } + /// Negotiate provider-signed agreement terms over HTTP. /// - /// The bucket owner POSTs the request shape they want; the provider - /// node returns SCALE-encoded [`AgreementTerms`](storage_primitives::AgreementTerms) - /// signed with its registered key. The result is fed directly into - /// [`AdminClient::establish_storage_agreement`](crate::admin::AdminClient::establish_storage_agreement) - /// to open the bucket + primary agreement on-chain. - /// - /// This is the on-ramp that replaced the old `request_agreement` + - /// `accept_agreement` two-step. - /// - /// # Parameters - /// - `provider_url`: Base HTTP URL of the provider node (no trailing slash). - /// - `req`: The negotiation request (owner, capacity, duration, validity). - /// - /// # Example - /// ```no_run - /// # use storage_client::{ProviderClient, NegotiateRequest}; - /// # async fn example() -> Result<(), Box> { - /// let signed = ProviderClient::negotiate_terms( - /// "http://provider.example:3333", - /// &NegotiateRequest { - /// owner: "5GrwvaEF...".parse()?, - /// max_bytes: 10 * 1024 * 1024 * 1024, - /// duration: 100_000, - /// valid_until_offset: 1_000, - /// }, - /// ).await?; - /// # Ok(()) - /// # } - /// ``` + /// Owner posts the proposed shape; the provider node allocates nonce + /// + validity window from its own state, signs, returns + /// [`SignedTerms`] ready for + /// [`AdminClient::establish_storage_agreement`](crate::admin::AdminClient::establish_storage_agreement). pub async fn negotiate_terms( provider_url: &str, req: &crate::agreement::NegotiateRequest, diff --git a/client/src/substrate.rs b/client/src/substrate.rs index 31f1e8c7..d0288d29 100644 --- a/client/src/substrate.rs +++ b/client/src/substrate.rs @@ -214,8 +214,14 @@ pub mod extrinsics { "owner", subxt::dynamic::Value::from_bytes(terms.owner.as_ref() as &[u8]), ), - ("max_bytes", subxt::dynamic::Value::u128(terms.max_bytes as u128)), - ("duration", subxt::dynamic::Value::u128(terms.duration as u128)), + ( + "max_bytes", + subxt::dynamic::Value::u128(terms.max_bytes as u128), + ), + ( + "duration", + subxt::dynamic::Value::u128(terms.duration as u128), + ), ( "price_per_byte", subxt::dynamic::Value::u128(terms.price_per_byte), @@ -843,6 +849,29 @@ pub mod storage { vec![subxt::dynamic::Value::u128(deadline_block as u128)], ) } + + /// Query the provider's replay-window state. + /// + /// Returns the storage address; the caller fetches it through subxt + /// and decodes the `ReplayWindow { hwm, bitmap }` payload. The + /// `hwm` field is what the provider node's nonce counter bootstraps + /// from on cold start so reissued nonces would land outside the + /// replay window and be rejected by Layer 0. + pub fn provider_replay_state( + account: &AccountId32, + ) -> subxt::storage::DefaultAddress< + Vec, + subxt::dynamic::DecodedValueThunk, + subxt::utils::Yes, + subxt::utils::Yes, + subxt::utils::Yes, + > { + subxt::dynamic::storage( + PALLET_NAME, + "ProviderReplayStates", + vec![subxt::dynamic::Value::from_bytes(account.as_ref() as &[u8])], + ) + } } // Helper functions for common operations diff --git a/client/tests/provider_integration.rs b/client/tests/provider_integration.rs index 96506d91..0c0d9ddc 100644 --- a/client/tests/provider_integration.rs +++ b/client/tests/provider_integration.rs @@ -414,4 +414,3 @@ async fn test_add_stake_increases_stake() { "stake should increase by exactly {increment} (before={stake_before}, after={stake_after})" ); } - diff --git a/pallet/src/benchmarking.rs b/pallet/src/benchmarking.rs index 2e210596..807ad239 100644 --- a/pallet/src/benchmarking.rs +++ b/pallet/src/benchmarking.rs @@ -143,12 +143,8 @@ fn setup_replica_agreement( replica_index: u32, ) { let key = register_sr25519_key::(replica, KEY_TYPE, replica_index); - let terms = build_replica_terms::( - admin, - 1_000_000u64, - 100u32.into(), - replica_index as u64 + 1, - ); + let terms = + build_replica_terms::(admin, 1_000_000u64, 100u32.into(), replica_index as u64 + 1); let sig = sign_terms::(&key, &terms); Pallet::::establish_replica_agreement_internal(admin, bucket_id, replica, terms, &sig) .expect("establish_replica_agreement_internal succeeds"); diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 88c70c6f..ea17a284 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -168,10 +168,10 @@ pub mod pallet { pub type Providers = StorageMap<_, Blake2_128Concat, T::AccountId, ProviderInfo>; /// Per-provider sliding replay window over signed agreement-term nonces. - /// See [`storage_primitives::ReplayWindow`] for the bit layout and - /// `establish_storage_agreement` for how the window is enforced. + /// See [`storage_primitives::ReplayWindow`] for the bit layout #[pallet::storage] - pub type ProviderReplayState = + #[pallet::getter(fn provider_replay_states)] + pub type ProviderReplayStates = StorageMap<_, Blake2_128Concat, T::AccountId, ReplayWindow, ValueQuery>; /// Monotonically increasing bucket ID counter. @@ -794,10 +794,6 @@ pub mod pallet { /// Account is a member of too many buckets. TooManyBucketsForMember, - /// The selected provider's price per byte exceeds the caller's - /// `max_price_per_byte`. - PriceExceedsMax, - // establish_storage_agreement errors /// Provider signature over the SCALE-encoded terms is invalid. InvalidProviderSignature, @@ -3772,7 +3768,7 @@ pub mod pallet { Self::verify_terms_signature(&provider_info, &terms, sig)?; // Replay window: at most once per nonce, within the trailing 256 slots. - ProviderReplayState::::try_mutate(provider, |window| -> DispatchResult { + ProviderReplayStates::::try_mutate(provider, |window| -> DispatchResult { window.try_accept(terms.nonce).map_err(|e| match e { ReplayError::AlreadyUsed => Error::::NonceAlreadyUsed, ReplayError::TooOld => Error::::NonceTooOld, @@ -3787,10 +3783,6 @@ pub mod pallet { Error::::ProviderNotAcceptingPrimary ); Self::validate_duration(&provider_info.settings, terms.duration)?; - ensure!( - provider_info.settings.price_per_byte <= terms.price_per_byte, - Error::::PriceExceedsMax - ); let new_committed = provider_info .committed_bytes @@ -3902,7 +3894,7 @@ pub mod pallet { Self::verify_terms_signature(&provider_info, &terms, sig)?; // Replay window: at most once per nonce, within the trailing 256 slots. - ProviderReplayState::::try_mutate(provider, |window| -> DispatchResult { + ProviderReplayStates::::try_mutate(provider, |window| -> DispatchResult { window.try_accept(terms.nonce).map_err(|e| match e { ReplayError::AlreadyUsed => Error::::NonceAlreadyUsed, ReplayError::TooOld => Error::::NonceTooOld, @@ -3917,10 +3909,6 @@ pub mod pallet { .replica_sync_price .ok_or(Error::::ProviderNotAcceptingReplicas)?; Self::validate_duration(&provider_info.settings, terms.duration)?; - ensure!( - provider_info.settings.price_per_byte <= terms.price_per_byte, - Error::::PriceExceedsMax - ); let new_committed = provider_info .committed_bytes diff --git a/provider-node/Cargo.toml b/provider-node/Cargo.toml index 4db5e4a4..d7d91b18 100644 --- a/provider-node/Cargo.toml +++ b/provider-node/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true description = "Off-chain provider node for scalable Web3 storage" [dependencies] +storage-client = { workspace = true } storage-primitives = { workspace = true, features = ["serde", "std"] } tokio = { workspace = true } axum = { workspace = true } @@ -15,6 +16,7 @@ tower-http = { workspace = true } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true } sp-core = { workspace = true, features = ["std"] } +sp-runtime = { workspace = true, features = ["std"] } subxt = { workspace = true } subxt-signer = { workspace = true } base64 = "0.22" diff --git a/provider-node/src/agreement_coordinator.rs b/provider-node/src/agreement_coordinator.rs deleted file mode 100644 index 3c503f90..00000000 --- a/provider-node/src/agreement_coordinator.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! Agreement Coordinator - Auto-accept pending agreement requests. -//! -//! This module provides a background service that polls for pending -//! `AgreementRequests` on-chain and automatically accepts them on behalf -//! of the provider. - -use crate::{Error, ProviderState}; -use sp_core::crypto::Ss58Codec; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use subxt::{dynamic::Value, OnlineClient, PolkadotConfig}; -use subxt_signer::sr25519::Keypair; -use tokio::sync::mpsc; - -/// Configuration for the agreement coordinator. -#[derive(Clone, Debug)] -pub struct AgreementCoordinatorConfig { - /// WebSocket URL for the parachain. - pub chain_ws_url: String, - /// How often to poll for pending agreement requests. - pub poll_interval: Duration, - /// Whether to automatically accept agreement requests. - pub auto_accept: bool, - /// Seed phrase or derivation path for signing (e.g., "//Alice"). - /// Used to create the subxt signer directly (avoids key conversion issues). - pub seed: Option, -} - -impl Default for AgreementCoordinatorConfig { - fn default() -> Self { - Self { - chain_ws_url: "ws://127.0.0.1:2222".to_string(), - poll_interval: Duration::from_secs(6), - auto_accept: true, - seed: None, - } - } -} - -/// Commands for controlling the coordinator. -#[derive(Debug)] -pub enum AgreementCommand { - /// Stop the coordinator. - Stop, -} - -/// Handle for controlling the agreement coordinator. -pub struct AgreementCoordinatorHandle { - command_tx: mpsc::Sender, - running: Arc, -} - -impl AgreementCoordinatorHandle { - /// Check if the coordinator is running. - pub fn is_running(&self) -> bool { - self.running.load(Ordering::SeqCst) - } - - /// Stop the coordinator. - pub async fn stop(&self) -> Result<(), Error> { - self.command_tx - .send(AgreementCommand::Stop) - .await - .map_err(|_| Error::Internal("Agreement coordinator channel closed".to_string())) - } -} - -/// Agreement coordinator service. -pub struct AgreementCoordinator { - config: AgreementCoordinatorConfig, - state: Arc, - api: Option>, - signer: Option, -} - -impl AgreementCoordinator { - /// Create a new agreement coordinator. - pub fn new(config: AgreementCoordinatorConfig, state: Arc) -> Self { - Self { - config, - state, - api: None, - signer: None, - } - } - - /// Connect to the blockchain. - pub async fn connect(&mut self) -> Result<(), Error> { - let api = OnlineClient::::from_url(&self.config.chain_ws_url) - .await - .map_err(|e| Error::Internal(format!("Failed to connect to chain: {e}")))?; - - self.api = Some(api); - - // Create signer from seed URI (e.g. "//Alice") using subxt_signer directly. - // This avoids key conversion issues between sp_core and subxt_signer. - if let Some(ref seed) = self.config.seed { - let uri: subxt_signer::SecretUri = seed - .parse() - .map_err(|e| Error::Internal(format!("Invalid seed URI: {e}")))?; - let signer = Keypair::from_uri(&uri) - .map_err(|e| Error::Internal(format!("Failed to create signer: {e}")))?; - tracing::info!( - "Agreement coordinator signer: {}", - sp_core::crypto::AccountId32::from(signer.public_key().0).to_ss58check() - ); - self.signer = Some(signer); - } - - tracing::info!( - "Agreement coordinator connected to {}", - self.config.chain_ws_url - ); - Ok(()) - } - - /// Start the agreement coordinator background service. - pub async fn start(self) -> Result { - if self.api.is_none() { - return Err(Error::Internal("Not connected to chain".to_string())); - } - if self.signer.is_none() { - return Err(Error::Internal( - "No signer available for agreement coordinator".to_string(), - )); - } - - let (command_tx, command_rx) = mpsc::channel::(32); - let running = Arc::new(AtomicBool::new(true)); - let running_clone = running.clone(); - - tokio::spawn(async move { - self.run_loop(command_rx, running_clone).await; - }); - - Ok(AgreementCoordinatorHandle { - command_tx, - running, - }) - } - - /// Main coordinator loop. - async fn run_loop( - self, - mut command_rx: mpsc::Receiver, - running: Arc, - ) { - let mut interval = tokio::time::interval(self.config.poll_interval); - - tracing::info!("Agreement coordinator started"); - - loop { - tokio::select! { - cmd = command_rx.recv() => { - match cmd { - Some(AgreementCommand::Stop) | None => { - tracing::info!("Agreement coordinator stopping"); - running.store(false, Ordering::SeqCst); - break; - } - } - } - _ = interval.tick() => { - if !self.config.auto_accept { - continue; - } - - if let Err(e) = self.poll_and_accept().await { - tracing::warn!("Agreement poll error: {}", e); - } - } - } - } - } - - /// Poll for pending agreement requests and accept them. - async fn poll_and_accept(&self) -> Result<(), Error> { - let api = self - .api - .as_ref() - .ok_or_else(|| Error::Internal("Not connected".to_string()))?; - let signer = self - .signer - .as_ref() - .ok_or_else(|| Error::Internal("No signer".to_string()))?; - - let provider_id = &self.state.provider_id; - - // Convert our SS58 provider ID to raw AccountId32 bytes for key comparison - let our_account: sp_core::crypto::AccountId32 = - sp_core::crypto::Ss58Codec::from_ss58check(provider_id) - .map_err(|e| Error::Internal(format!("Invalid provider SS58 address: {e:?}")))?; - let our_bytes: [u8; 32] = our_account.into(); - - // Iterate ALL AgreementRequests entries on chain. - // Storage layout: DoubleMap - // Key bytes: [16 pallet_hash][16 storage_hash][16 blake2_hash + 8 bucket_id][16 blake2_hash + 32 account] - // Total = 32 (prefix) + 24 (key1) + 48 (key2) = 104 bytes - let storage_query = subxt::dynamic::storage("StorageProvider", "AgreementRequests", ()); - let storage = api - .storage() - .at_latest() - .await - .map_err(|e| Error::Internal(format!("Failed to get storage: {e}")))?; - - let mut entries = storage - .iter(storage_query) - .await - .map_err(|e| Error::Internal(format!("Failed to iterate agreement requests: {e}")))?; - - let mut bucket_ids_to_accept: Vec = Vec::new(); - let mut entry_count = 0u32; - - while let Some(result) = entries.next().await { - let entry = match result { - Ok(e) => e, - Err(e) => { - tracing::warn!("Error reading agreement request entry: {}", e); - continue; - } - }; - - entry_count += 1; - let key_bytes = &entry.key_bytes; - let key_len = key_bytes.len(); - - // Expected key length: 32 (prefix) + 24 (key1) + 48 (key2) = 104 - if key_len < 104 { - tracing::warn!("Unexpected key length {} (expected 104), skipping", key_len); - continue; - } - - // Account bytes at offset 72 (32 prefix + 16 blake2 + 8 bucket + 16 blake2) - let account_bytes = &key_bytes[72..104]; - - // Check if this request is for our provider - if account_bytes != our_bytes.as_slice() { - continue; - } - - // Bucket ID at offset 48 (32 prefix + 16 blake2 hash) - let bucket_id = u64::from_le_bytes( - key_bytes[48..56] - .try_into() - .expect("slice is exactly 8 bytes"), - ); - - tracing::info!( - "Found pending agreement request for us: bucket {}", - bucket_id - ); - - bucket_ids_to_accept.push(bucket_id); - } - - if entry_count > 0 { - tracing::info!( - "Scanned {} agreement request entries, {} for us", - entry_count, - bucket_ids_to_accept.len() - ); - } - - // Accept each pending request - for bucket_id in bucket_ids_to_accept { - tracing::info!("Auto-accepting agreement for bucket {}", bucket_id); - - let tx = subxt::dynamic::tx( - "StorageProvider", - "accept_agreement", - vec![Value::u128(bucket_id as u128)], - ); - - match api - .tx() - .sign_and_submit_then_watch_default(&tx, signer) - .await - { - Ok(progress) => match progress.wait_for_finalized_success().await { - Ok(_events) => { - tracing::info!( - "Auto-accepted agreement for bucket {} (finalized)", - bucket_id - ); - } - Err(e) => { - tracing::warn!( - "accept_agreement tx failed for bucket {}: {}", - bucket_id, - e - ); - } - }, - Err(e) => { - tracing::warn!( - "Failed to submit accept_agreement for bucket {}: {}", - bucket_id, - e - ); - } - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = AgreementCoordinatorConfig::default(); - assert_eq!(config.chain_ws_url, "ws://127.0.0.1:2222"); - assert_eq!(config.poll_interval, Duration::from_secs(6)); - assert!(config.auto_accept); - } - - #[test] - fn test_coordinator_creation() { - let storage = Arc::new(crate::Storage::new()); - let state = Arc::new(crate::ProviderState::new( - storage, - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".to_string(), - )); - let config = AgreementCoordinatorConfig::default(); - let coordinator = AgreementCoordinator::new(config, state); - assert!(coordinator.api.is_none()); - assert!(coordinator.signer.is_none()); - } -} diff --git a/provider-node/src/api.rs b/provider-node/src/api.rs index 04b84bec..7ff1f259 100644 --- a/provider-node/src/api.rs +++ b/provider-node/src/api.rs @@ -6,6 +6,7 @@ use crate::checkpoint_coordinator::{ }; use crate::error::Error; use crate::fs_api; +use crate::negotiate::{self, AgreementTermsOf, NegotiateRequest, SignedTerms}; use crate::s3_api; use crate::storage::{hex_decode, hex_encode}; use crate::types::*; @@ -19,6 +20,7 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use codec::Encode; use sp_core::{Pair, H256}; use std::sync::Arc; +use storage_primitives::AgreementTerms; use storage_primitives::{CheckpointProposal, CommitmentPayload}; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; @@ -48,6 +50,8 @@ pub fn create_router(state: Arc) -> Router { .route("/mmr_peaks", get(get_mmr_peaks)) .route("/mmr_subtree", get(get_mmr_subtree)) .route("/fetch_nodes", post(fetch_nodes)) + // Off-chain term negotiation (signed AgreementTerms for `establish_storage_agreement`) + .route("/negotiate", post(negotiate_terms)) // Checkpoint coordination .route("/checkpoint/sign", post(sign_checkpoint_proposal)) .route("/checkpoint/duty", get(get_checkpoint_duty)) @@ -738,6 +742,37 @@ async fn get_historical_roots( })) } +/// Negotiate provider-signed [`AgreementTerms`] for a bucket owner. +/// +/// TODO: Request are automatically accepted, implement advance features to let providers determine +/// +/// Returns `503` if the node has no signing key (no `--keyfile`) or no +async fn negotiate_terms( + State(state): State>, + Json(req): Json, +) -> Result, Error> { + let keypair = state.keypair.as_ref().ok_or_else(|| { + Error::Internal("provider node has no signing key; /negotiate disabled".to_string()) + })?; + let nonce_counter = state.nonce_counter.as_ref().ok_or_else(|| { + Error::Internal("provider node has no nonce counter; /negotiate disabled".to_string()) + })?; + + let terms: AgreementTermsOf = AgreementTerms { + owner: req.owner, + max_bytes: req.max_bytes, + duration: req.duration, + price_per_byte: req.price_per_byte, + // TODO: lookup current block and use a bounded offset. + valid_until: u32::MAX, + nonce: nonce_counter.next(), + replica_params: req.replica_params, + }; + let signature = negotiate::sign_terms(keypair, &terms); + + Ok(Json(SignedTerms { terms, signature })) +} + /// Get replica sync status for a bucket. /// /// Returns the local MMR state and sync status. diff --git a/provider-node/src/cli.rs b/provider-node/src/cli.rs index 2519be3a..6fbd4fcc 100644 --- a/provider-node/src/cli.rs +++ b/provider-node/src/cli.rs @@ -34,9 +34,6 @@ pub struct Cli { #[clap(flatten)] pub replica_sync: ReplicaSyncParams, - #[clap(flatten)] - pub agreement: AgreementParams, - #[clap(flatten)] pub auth: AuthParams, } @@ -154,24 +151,6 @@ pub struct CheckpointParams { pub enable_checkpoint_coordinator: bool, } -/// Parameters for the agreement coordinator. -#[derive(Debug, clap::Args)] -pub struct AgreementParams { - /// Enable the background agreement coordinator that auto-accepts - /// pending storage agreement requests. - #[arg(long, env = "ENABLE_AGREEMENT_COORDINATOR")] - pub enable_agreement_coordinator: bool, - - /// Seconds between agreement poll checks. - #[arg( - long, - value_name = "SECONDS", - default_value_t = 6, - env = "AGREEMENT_POLL_INTERVAL" - )] - pub agreement_poll_interval: u64, -} - /// Parameters for authentication and authorization. #[derive(Debug, clap::Args)] pub struct AuthParams { @@ -247,8 +226,6 @@ mod tests { assert!(cli.key.keyfile.is_none()); assert!(cli.key.provider_id.is_none()); assert!(!cli.checkpoint.enable_checkpoint_coordinator); - assert!(!cli.agreement.enable_agreement_coordinator); - assert_eq!(cli.agreement.agreement_poll_interval, 6); assert!(!cli.replica_sync.enable_replica_sync); assert_eq!(cli.replica_sync.replica_poll_interval, 12); assert_eq!(cli.replica_sync.replica_sync_timeout, 300); @@ -270,9 +247,6 @@ mod tests { "--keyfile", "/tmp/test-key", "--enable-checkpoint-coordinator", - "--enable-agreement-coordinator", - "--agreement-poll-interval", - "10", "--enable-replica-sync", "--replica-poll-interval", "30", @@ -292,8 +266,6 @@ mod tests { "/tmp/test-key" ); assert!(cli.checkpoint.enable_checkpoint_coordinator); - assert!(cli.agreement.enable_agreement_coordinator); - assert_eq!(cli.agreement.agreement_poll_interval, 10); assert!(cli.replica_sync.enable_replica_sync); assert_eq!(cli.replica_sync.replica_poll_interval, 30); assert_eq!(cli.replica_sync.replica_sync_timeout, 600); diff --git a/provider-node/src/command.rs b/provider-node/src/command.rs index bfe3e483..9bf8d27d 100644 --- a/provider-node/src/command.rs +++ b/provider-node/src/command.rs @@ -3,12 +3,12 @@ use crate::{ auth::{ChainMembershipResolver, MembershipCache}, cli::{Cli, StorageMode, DEFAULT_PROVIDER_ID}, - create_router, AgreementCoordinator, AgreementCoordinatorConfig, AgreementCoordinatorHandle, - CheckpointCoordinator, CheckpointCoordinatorConfig, CheckpointCoordinatorHandle, DiskStorage, - ProviderState, ReplicaSyncCoordinator, ReplicaSyncCoordinatorConfig, + create_router, CheckpointCoordinator, CheckpointCoordinatorConfig, CheckpointCoordinatorHandle, + DiskStorage, NonceCounter, ProviderState, ReplicaSyncCoordinator, ReplicaSyncCoordinatorConfig, ReplicaSyncCoordinatorHandle, Storage, StorageBackend, }; use clap::Parser; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use subxt::dynamic::At; @@ -64,6 +64,8 @@ pub async fn run() -> Result<(), Box> { ); } + state.nonce_counter = Some(setup_nonce_counter(&cli, &state.provider_id).await?); + Arc::new(state) } None => { @@ -103,7 +105,6 @@ pub async fn run() -> Result<(), Box> { state.set_checkpoint_handle(handle); } let _replica_sync_handle = start_replica_sync_coordinator(&cli, state.clone()).await; - let _agreement_handle = start_agreement_coordinator(&cli, &seed, state.clone()).await; // Sync on-chain multiaddr with actual bind address (requires signing key) if let Some(seed) = &seed { @@ -204,48 +205,60 @@ async fn start_replica_sync_coordinator( } } -async fn start_agreement_coordinator( +/// Open the persistent nonce counter and bootstrap it from the chain's +/// `ProviderReplayState.hwm`. Default fallback to local storage, starting from 0 +async fn setup_nonce_counter( cli: &Cli, - seed: &Option, - state: Arc, -) -> Option { - if !cli.agreement.enable_agreement_coordinator { - return None; - } - - let seed = match seed { - Some(s) => s.clone(), - None => { - tracing::error!("Agreement coordinator requires --keyfile for signing. Skipping."); - return None; + provider_id: &str, +) -> Result, Box> { + let counter = match cli.storage.storage_mode { + StorageMode::Inmemory => NonceCounter::in_memory(0), + StorageMode::Disk => { + let nonce_path = cli.storage.storage_path.join("nonce"); + if let Some(parent) = nonce_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + format!( + "failed to create nonce-counter parent dir {:?}: {}", + parent, e, + ) + })?; + } + NonceCounter::open(nonce_path.clone()) + .map_err(|e| format!("failed to open nonce counter at {:?}: {}", nonce_path, e))? } }; - let config = AgreementCoordinatorConfig { - chain_ws_url: cli.rpc.chain_rpc.clone(), - poll_interval: Duration::from_secs(cli.agreement.agreement_poll_interval), - auto_accept: true, - seed: Some(seed), - }; - - let mut coordinator = AgreementCoordinator::new(config, state); - - if let Err(e) = coordinator.connect().await { - tracing::error!("Failed to connect agreement coordinator: {}", e); - return None; - } - tracing::info!("Agreement coordinator connected to chain"); - - match coordinator.start().await { - Ok(handle) => { - tracing::info!("Agreement coordinator started — auto-accepting agreements"); - Some(handle) + // Bootstrap from on-chain hwm. Best-effort: if the chain isn't + // reachable yet, fall back to the local counter — the on-chain + // replay window will reject any out-of-range reissues anyway. + let provider_account = sp_runtime::AccountId32::from_str(provider_id) + .map_err(|e| format!("invalid provider SS58: {e:?}"))?; + match storage_client::ProviderClient::fetch_replay_hwm(&cli.rpc.chain_rpc, &provider_account) + .await + { + Ok(Some(hwm)) => { + tracing::info!( + "Bootstrapping nonce counter from on-chain hwm {} for provider {}", + hwm, + provider_id, + ); + counter.bootstrap_from_hwm(hwm); + } + Ok(None) => { + tracing::info!( + "No on-chain replay state for provider {} yet; starting nonce counter from local storage", + provider_id, + ); } Err(e) => { - tracing::error!("Failed to start agreement coordinator: {}", e); - None + tracing::warn!( + "Failed to bootstrap nonce counter from chain: {}; falling back to local storage", + e, + ); } } + + Ok(Arc::new(counter)) } /// Convert a bind address (e.g. "0.0.0.0:3333") to a multiaddr string (e.g. "/ip4/127.0.0.1/tcp/3333"). diff --git a/provider-node/src/lib.rs b/provider-node/src/lib.rs index 59f6fb8b..1ec7b4e8 100644 --- a/provider-node/src/lib.rs +++ b/provider-node/src/lib.rs @@ -8,7 +8,6 @@ //! - Syncing data between providers (for replicas) //! - Coordinating provider-initiated checkpoints -pub mod agreement_coordinator; pub mod api; pub mod auth; pub mod challenge_responder; @@ -19,6 +18,7 @@ pub mod error; pub mod fs_api; pub mod fs_index; pub mod mmr; +pub mod negotiate; pub mod replica_sync; pub mod replica_sync_coordinator; pub mod s3_api; @@ -26,9 +26,6 @@ pub mod s3_index; pub mod storage; pub mod types; -pub use agreement_coordinator::{ - AgreementCoordinator, AgreementCoordinatorConfig, AgreementCoordinatorHandle, -}; pub use api::create_router; pub use challenge_responder::{ ChallengeResponder, ChallengeResponderConfig, ChallengeResponderHandle, @@ -40,6 +37,7 @@ pub use checkpoint_coordinator::{ }; pub use error::Error; pub use fs_index::FsIndexManager; +pub use negotiate::{sign_terms, AgreementTermsOf, NegotiateRequest, NonceCounter, SignedTerms}; pub use replica_sync::ReplicaSync; pub use replica_sync_coordinator::{ ReplicaSyncCoordinator, ReplicaSyncCoordinatorConfig, ReplicaSyncCoordinatorHandle, @@ -77,6 +75,9 @@ pub struct ProviderState { pub membership_cache: Option>, /// Maximum allowed clock skew for request timestamps. pub auth_max_skew: Duration, + /// Monotonic nonce counter used by `/negotiate` to allocate fresh + /// nonces for provider-signed `AgreementTerms`. + pub nonce_counter: Option>, } impl ProviderState { @@ -91,6 +92,7 @@ impl ProviderState { auth_enabled: false, membership_cache: None, auth_max_skew: Duration::from_secs(300), + nonce_counter: None, } } @@ -111,6 +113,7 @@ impl ProviderState { auth_enabled: false, membership_cache: None, auth_max_skew: Duration::from_secs(300), + nonce_counter: None, }) } diff --git a/provider-node/src/negotiate.rs b/provider-node/src/negotiate.rs new file mode 100644 index 00000000..e94677ec --- /dev/null +++ b/provider-node/src/negotiate.rs @@ -0,0 +1,159 @@ +//! Off-chain terms negotiation — provider-signed [`AgreementTerms`]. +//! +//! Bucket owners ask the provider node for signed terms via +//! `POST /negotiate`. The provider node: +//! +//! 1. Allocates a fresh nonce from a persistent monotonic counter +//! ([`NonceCounter`]). The counter is initialized at startup from the +//! chain's `ProviderReplayState.hwm + 1`, so duplicates can't survive a +//! restart that lost the local file (the on-chain replay window will +//! reject them). +//! 2. Builds [`AgreementTerms`] from the request, the provider's current +//! `price_per_byte` setting (read from chain), and +//! `valid_until = current_block + valid_until_offset`. +//! 3. Signs `blake2_256(SCALE(terms))` with the provider's existing +//! sr25519 checkpoint key (the same one used to sign commitments). + +use codec::Encode; +use sp_core::Pair; +use sp_runtime::MultiSignature; +use std::fs; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; + +// Wire types are shared with the SDK so client + server agree on serde shape. +pub use storage_client::agreement::{AgreementTermsOf, NegotiateRequest, SignedTerms}; + +/// Persistent monotonic nonce counter for provider-signed terms. +/// +/// Nonces are atomically allocated via [`Self::next`]. Each allocation +/// writes the new value to `path` synchronously so a crash mid-handler +/// can't reissue the same nonce. +/// +/// At startup, the caller should reconcile against the chain by calling +/// [`Self::bootstrap_from_hwm`] with the provider's on-chain `hwm`. The +/// counter then resumes at `max(local, hwm + 1)`, which: +/// +/// * survives a restart that lost the local file (uses chain hwm); +/// * survives a restart where the chain advanced past our local view +/// (e.g. a parallel quote was redeemed elsewhere) — we skip past it +/// rather than reissue. +/// +/// Gap-skipping is fine: unused nonces just expire from the replay +/// window without effect. +#[derive(Debug)] +pub struct NonceCounter { + counter: AtomicU64, + path: Option, +} + +impl NonceCounter { + /// In-memory counter (testing). No persistence. + pub fn in_memory(start: u64) -> Self { + Self { + counter: AtomicU64::new(start), + path: None, + } + } + + /// Counter backed by a file. Reads the existing value if present, + /// otherwise starts at 0. + pub fn open(path: PathBuf) -> std::io::Result { + let start = match fs::read_to_string(&path) { + Ok(s) => s.trim().parse::().unwrap_or(0), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => 0, + Err(e) => return Err(e), + }; + Ok(Self { + counter: AtomicU64::new(start), + path: Some(path), + }) + } + + /// Advance the counter to at least `hwm + 1`. Idempotent — only + /// advances forward. + pub fn bootstrap_from_hwm(&self, hwm: u64) { + let target = hwm.saturating_add(1); + // Standard CAS loop — bump only if our target is higher than + // whatever is already there. + let mut current = self.counter.load(Ordering::SeqCst); + while current < target { + match self.counter.compare_exchange_weak( + current, + target, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => break, + Err(observed) => current = observed, + } + } + self.persist(); + } + + /// Allocate the next nonce. Atomic + persistent: even concurrent + /// callers each get a distinct value, and the on-disk file always + /// trails (or matches) the highest issued nonce. + pub fn next(&self) -> u64 { + let n = self.counter.fetch_add(1, Ordering::SeqCst); + self.persist(); + n + } + + /// Best-effort persist of the *next* value to disk. Failures are + /// logged but don't fail the call — the chain's replay window is + /// authoritative anyway. + fn persist(&self) { + let Some(ref path) = self.path else { return }; + let next = self.counter.load(Ordering::SeqCst); + if let Err(e) = fs::write(path, next.to_string()) { + tracing::warn!("Failed to persist nonce counter to {:?}: {}", path, e); + } + } +} + +/// Sign agreement terms with the provider's checkpoint sr25519 key. +/// +/// Mirrors the on-chain verifier: SCALE-encode → blake2-256 → sr25519 +/// sign → wrap as `MultiSignature::Sr25519`. +pub fn sign_terms(keypair: &sp_core::sr25519::Pair, terms: &AgreementTermsOf) -> MultiSignature { + let hash = sp_core::hashing::blake2_256(&terms.encode()); + let sig = keypair.sign(&hash); + MultiSignature::Sr25519(sig) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nonce_counter_in_memory_is_monotonic() { + let c = NonceCounter::in_memory(0); + assert_eq!(c.next(), 0); + assert_eq!(c.next(), 1); + assert_eq!(c.next(), 2); + } + + #[test] + fn bootstrap_from_hwm_only_advances() { + let c = NonceCounter::in_memory(10); + c.bootstrap_from_hwm(5); // lower than current — no-op + assert_eq!(c.next(), 10); + c.bootstrap_from_hwm(20); // higher — advance + assert_eq!(c.next(), 21); + } + + #[test] + fn persists_and_resumes_from_disk() { + let dir = std::env::temp_dir().join(format!("nonce-counter-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("nonce"); + let c = NonceCounter::open(path.clone()).unwrap(); + assert_eq!(c.next(), 0); + assert_eq!(c.next(), 1); + drop(c); + // Reopening should pick up where we left off. + let c2 = NonceCounter::open(path).unwrap(); + assert_eq!(c2.next(), 2); + } +} diff --git a/provider-node/tests/complete_workflow.rs b/provider-node/tests/complete_workflow.rs new file mode 100644 index 00000000..c43f4ba6 --- /dev/null +++ b/provider-node/tests/complete_workflow.rs @@ -0,0 +1,186 @@ +//! End-to-end happy path +//! 1. register provider +//! 2. negotiate primary provider +//! 3. establish agreement, create bucket on-chain +//! 4. upload data +//! 5. download data +//! +//! Steps to run the test +//! 1. just start-paseo-chain +//! 2. cargo test -p storage-provider-node --test complete_workflow -- --no-capture + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use sp_runtime::AccountId32; +use storage_client::{ + AdminClient, ChunkingStrategy, ClientConfig, NegotiateRequest, ProviderClient, + ProviderSettings, SignedTerms, StorageUserClient, +}; +use storage_provider_node::{create_router, NonceCounter, ProviderState, Storage}; +use subxt_signer::sr25519::{dev as dev_signer, Keypair}; +use tokio::net::TcpListener; + +const CHAIN_WS: &str = "ws://127.0.0.1:2222"; +/// 1000 tokens × 12 decimals — covers the runtime's `MinProviderStake`. +const PROVIDER_STAKE: u128 = 1_000 * 1_000_000_000_000u128; + +/// Bundles a dev-account's seed, dev-signer name, keypair, and derived +/// account id so the rest of the test pulls everything from one place. +struct DevIdentity { + seed: &'static str, + dev_name: &'static str, + keypair: Keypair, +} + +impl DevIdentity { + fn provider() -> Self { + Self { + seed: "//Bob", + dev_name: "bob", + keypair: dev_signer::bob(), + } + } + + fn admin() -> Self { + Self { + seed: "//Alice", + dev_name: "alice", + keypair: dev_signer::alice(), + } + } + + fn account(&self) -> AccountId32 { + AccountId32::from(self.keypair.public_key().0) + } + + fn ss58(&self) -> String { + self.account().to_string() + } + + fn public_key_bytes(&self) -> Vec { + self.keypair.public_key().0.to_vec() + } +} + +fn chain_config() -> ClientConfig { + ClientConfig { + chain_ws_url: CHAIN_WS.to_string(), + provider_urls: vec![], + timeout_secs: 30, + enable_retries: false, + } +} + +/// In-process provider node signed with `id`'s seed. +async fn start_provider(id: &DevIdentity) -> (String, SocketAddr) { + let storage = Arc::new(Storage::new()); + let mut state = ProviderState::with_seed(storage, id.seed).expect("provider keypair"); + state.nonce_counter = Some(Arc::new({ + let c = NonceCounter::in_memory(0); + c.bootstrap_from_hwm(0); + c + })); + let state = Arc::new(state); + let app = create_router(state); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + tokio::time::sleep(Duration::from_millis(20)).await; + (format!("http://{addr}"), addr) +} + +/// Register `id` on-chain with the same sr25519 key its in-process provider +/// node uses for signing. Idempotent. +async fn ensure_provider_registered(id: &DevIdentity) -> Result<(), Box> { + let account = id.account(); + let mut provider = ProviderClient::new(chain_config(), id.ss58())?; + provider.connect().await?; + provider.set_dev_signer(id.dev_name)?; + + if provider.get_provider_info(&account).await?.is_none() { + provider + .register( + "/ip4/127.0.0.1/tcp/3333".to_string(), + id.public_key_bytes(), + PROVIDER_STAKE, + ) + .await?; + + provider + .update_settings(ProviderSettings { + price_per_byte: 0, + min_duration: 1, + max_duration: 1_000_000, + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + max_capacity: 0, + }) + .await?; + } + Ok(()) +} + +#[tokio::test] +async fn complete_workflow_e2e() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + let provider_id = DevIdentity::provider(); + let admin_id = DevIdentity::admin(); + + tracing::info!("Ensuring provider registered on chain ..."); + ensure_provider_registered(&provider_id).await?; + tracing::info!("Provider is already registered!"); + + tracing::info!("Starting up provider"); + let (provider_url, _addr) = start_provider(&provider_id).await; + + tracing::info!("Admin submit `NegotiateRequest` and get provider's signature back."); + // 1. Negotiate terms via the in-process provider node. + let signed: SignedTerms = ProviderClient::negotiate_terms( + &provider_url, + &NegotiateRequest { + owner: admin_id.account(), + max_bytes: 1_000_000, + duration: 100, + price_per_byte: 0, + replica_params: None, + }, + ) + .await?; + assert!(signed.terms.nonce > 0, "provider should allocate nonce"); + + tracing::info!( + "Admin establish storage agreement with primary provider and create on chain bucket." + ); + // 2. Redeem on-chain. + let mut admin = AdminClient::new(chain_config(), admin_id.ss58())?; + admin.connect().await?; + admin.set_dev_signer(admin_id.dev_name)?; + let bucket_id = admin + .establish_storage_agreement(provider_id.ss58(), signed.terms, signed.signature) + .await?; + tracing::info!("Bucket #{bucket_id} is created"); + + tracing::info!("User upload & download the data, verify data integrity."); + // 3. Upload + download against the same in-process provider node. + let user_config = ClientConfig { + chain_ws_url: CHAIN_WS.to_string(), + provider_urls: vec![provider_url.clone()], + timeout_secs: 30, + enable_retries: false, + }; + let user = StorageUserClient::new(user_config)?; + let data = b"hello e2e".to_vec(); + let data_root = user + .upload(bucket_id, &data, ChunkingStrategy::default()) + .await?; + let downloaded = user.download(&data_root, 0, data.len() as u64).await?; + assert_eq!(downloaded, data, "downloaded bytes must match upload"); + Ok(()) +} diff --git a/runtimes/web3-storage-paseo/tests/tests.rs b/runtimes/web3-storage-paseo/tests/tests.rs index f6994f04..145f64f9 100644 --- a/runtimes/web3-storage-paseo/tests/tests.rs +++ b/runtimes/web3-storage-paseo/tests/tests.rs @@ -831,8 +831,8 @@ fn drive_lifecycle_e2e() { }), )); - let drive = pallet_drive_registry::Drives::::get(drive_id) - .expect("drive must be stored"); + let drive = + pallet_drive_registry::Drives::::get(drive_id).expect("drive must be stored"); assert_eq!(drive.owner, owner_id); assert_eq!(drive.max_capacity, 1_000_000); assert!(pallet_drive_registry::UserDrives::::get(&owner_id).contains(&drive_id)); @@ -932,8 +932,8 @@ fn drive_lifecycle_via_xcm_e2e() { .ensure_complete() ); - let drive = pallet_drive_registry::Drives::::get(drive_id) - .expect("drive must be stored"); + let drive = + pallet_drive_registry::Drives::::get(drive_id).expect("drive must be stored"); assert_eq!(drive.owner, derived); assert_eq!(drive.max_capacity, 1_000_000); assert!(pallet_drive_registry::UserDrives::::get(&derived).contains(&drive_id)); @@ -1041,8 +1041,8 @@ fn s3_bucket_lifecycle_e2e() { let bucket = pallet_s3_registry::S3Buckets::::get(s3_bucket_id).unwrap(); assert_eq!(bucket.object_count, 1); assert_eq!(bucket.total_size, 1024); - let obj = S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg") - .expect("object must be stored"); + let obj = + S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").expect("object must be stored"); assert_eq!(obj.cid, cid); assert_eq!(obj.size, 1024); @@ -1141,7 +1141,9 @@ fn s3_bucket_lifecycle_via_xcm_e2e() { .ensure_complete() ); assert_eq!( - S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg").unwrap().cid, + S3Registry::get_object(s3_bucket_id, b"photos/cat.jpg") + .unwrap() + .cid, cid ); diff --git a/storage-interfaces/file-system/pallet-registry/src/lib.rs b/storage-interfaces/file-system/pallet-registry/src/lib.rs index 54db39ad..172359f9 100644 --- a/storage-interfaces/file-system/pallet-registry/src/lib.rs +++ b/storage-interfaces/file-system/pallet-registry/src/lib.rs @@ -389,8 +389,7 @@ pub mod pallet { #[allow(clippy::type_complexity)] pub fn get_drive( drive_id: DriveId, - ) -> Option, T::MaxDriveNameLength>> - { + ) -> Option, T::MaxDriveNameLength>> { Drives::::get(drive_id) } diff --git a/storage-interfaces/file-system/pallet-registry/src/tests.rs b/storage-interfaces/file-system/pallet-registry/src/tests.rs index a223daf4..730b23e5 100644 --- a/storage-interfaces/file-system/pallet-registry/src/tests.rs +++ b/storage-interfaces/file-system/pallet-registry/src/tests.rs @@ -1,5 +1,5 @@ use crate::{ - mock::{*, MaxMultiaddrLength}, + mock::{MaxMultiaddrLength, *}, Drives, Error, }; use codec::Encode; @@ -90,15 +90,17 @@ fn create_drive_works() { assert_eq!(drive.owner, 1); assert_eq!(drive.max_capacity, 100); assert_eq!(drive.storage_period, 500); - assert_eq!(drive.name.as_ref().map(|n| n.to_vec()), Some(b"My Documents".to_vec())); + assert_eq!( + drive.name.as_ref().map(|n| n.to_vec()), + Some(b"My Documents".to_vec()) + ); // Layer 0 bucket exists with the provider as the lone primary. let l0_bucket = pallet_storage_provider::Buckets::::get(drive.bucket_id).unwrap(); assert_eq!(l0_bucket.primary_providers.to_vec(), vec![3]); - assert!(pallet_storage_provider::StorageAgreements::::contains_key( - drive.bucket_id, - 3, - )); + assert!( + pallet_storage_provider::StorageAgreements::::contains_key(drive.bucket_id, 3,) + ); }); } @@ -133,13 +135,7 @@ fn create_drive_name_too_long_fails() { let long_name = vec![b'a'; 257]; // MaxDriveNameLength = 256 in mock assert_noop!( - DriveRegistry::create_drive( - RuntimeOrigin::signed(1), - Some(long_name), - 3, - terms, - sig, - ), + DriveRegistry::create_drive(RuntimeOrigin::signed(1), Some(long_name), 3, terms, sig,), Error::::DriveNameTooLong ); }); diff --git a/storage-interfaces/file-system/primitives/src/lib.rs b/storage-interfaces/file-system/primitives/src/lib.rs index 083ae74e..87dbdfb1 100644 --- a/storage-interfaces/file-system/primitives/src/lib.rs +++ b/storage-interfaces/file-system/primitives/src/lib.rs @@ -609,7 +609,7 @@ pub enum FileSystemError { pub struct DriveInfo< AccountId: Encode + Decode + MaxEncodedLen, BlockNumber: Encode + Decode + MaxEncodedLen, - MaxNameLength: Get + MaxNameLength: Get, > { /// Owner of the drive pub owner: AccountId, diff --git a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs index 3c7c7162..58cd0ed4 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs @@ -28,9 +28,7 @@ use frame_support::{ BoundedVec, }; use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin}; -use pallet_storage_provider::{ - AgreementTermsOf, Pallet as StorageProvider, ProviderSettings, -}; +use pallet_storage_provider::{AgreementTermsOf, Pallet as StorageProvider, ProviderSettings}; use s3_primitives::{ BucketName, MaxContentTypeLen, MaxEtagLen, MaxMetadataEntries, MaxMetadataKeyLen, MaxMetadataValueLen, MaxObjectKeyLen, MetadataEntry, ObjectKey, ObjectMetadata, S3BucketId, diff --git a/storage-interfaces/s3/pallet-s3-registry/src/lib.rs b/storage-interfaces/s3/pallet-s3-registry/src/lib.rs index f5e9ba18..2b5a96aa 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/lib.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/lib.rs @@ -482,7 +482,6 @@ pub mod pallet { Ok(()) } - } impl Pallet { From 52e3d59b34a23c09dc98fecf70f20977f4b98031 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 29 May 2026 16:53:00 +0700 Subject: [PATCH 13/44] feat: update full-flow papi example --- examples/papi/api.js | 105 +++++++++++++++++++++---------------- examples/papi/full-flow.js | 48 +++++++++-------- justfile | 2 - 3 files changed, 84 insertions(+), 71 deletions(-) diff --git a/examples/papi/api.js b/examples/papi/api.js index b559b362..7682ae82 100644 --- a/examples/papi/api.js +++ b/examples/papi/api.js @@ -34,61 +34,74 @@ export async function updateProviderSettings(api, provider, settings) { ); } -export async function createBucket(api, signer, { minProviders = 1 } = {}) { - const result = await submitTx( - api.tx.StorageProvider.create_bucket({ min_providers: minProviders }), - signer.signer, - "create_bucket" - ); - const event = requireOneEvent( - result.events, - api.event.StorageProvider.BucketCreated, - "BucketCreated" - ); - return event.bucket_id; +/** + * POST /negotiate on the provider node; returns the SignedTerms bundle the + * owner redeems via `establish_storage_agreement`. + */ +export async function negotiateTerms(providerUrl, request) { + const res = await fetch(`${providerUrl}/negotiate`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + }); + if (!res.ok) { + throw new Error( + `/negotiate failed: ${res.status} ${await res.text().catch(() => "")}` + ); + } + return res.json(); } -export async function createBucketWithStorage(api, client, params) { +/** + * Submit `establish_storage_agreement` with provider-signed terms. + * + * `signed.signature` is the SCALE-encoded `MultiSignature` as hex, e.g. + * `0x00<64-byte-sig>` for Sr25519. Strip the variant prefix and wrap the + * raw 64 bytes back into the PAPI Enum. + */ +const MULTI_SIGNATURE_VARIANT = Object.freeze({ + 0: "Ed25519", + 1: "Sr25519", + 2: "ecdsa", + 3: "eth", +}); +export async function establishStorageAgreement(api, client, provider, signed) { + const sigBytes = hexToBytes(signed.signature); + if (sigBytes.length < 1) { + throw new Error("signature too short to contain a MultiSignature variant byte"); + } + const variantIdx = sigBytes[0]; + const inner = sigBytes.slice(1); + const variantName = MULTI_SIGNATURE_VARIANT[variantIdx]; + if (!variantName) { + throw new Error(`unknown MultiSignature variant byte: ${variantIdx}`); + } + const sigVariant = Enum(variantName, Binary.fromBytes(inner)); + + const t = signed.terms; const result = await submitTx( - api.tx.StorageProvider.create_bucket_with_storage(params), + api.tx.StorageProvider.establish_storage_agreement({ + provider: provider.address, + terms: { + owner: t.owner, + max_bytes: BigInt(t.max_bytes), + duration: t.duration, + price_per_byte: BigInt(t.price_per_byte), + valid_until: t.valid_until, + nonce: BigInt(t.nonce), + replica_params: t.replica_params ?? undefined, + }, + sig: sigVariant, + }), client.signer, - "create_bucket_with_storage" + "establish_storage_agreement" ); - const created = requireOneEvent( + const event = requireOneEvent( result.events, api.event.StorageProvider.BucketCreated, "BucketCreated" ); - const accepted = requireOneEvent( - result.events, - api.event.StorageProvider.AgreementAccepted, - "AgreementAccepted" - ); - return { - bucketId: created.bucket_id, - matchedProvider: accepted.provider, - expiresAt: accepted.expires_at, - }; -} - -export async function requestPrimaryAgreement(api, client, provider, bucketId, params) { - return submitTx( - api.tx.StorageProvider.request_primary_agreement({ - bucket_id: bucketId, - provider: provider.address, - ...params, - }), - client.signer, - "request_primary_agreement" - ); -} - -export async function acceptAgreement(api, provider, bucketId) { - return submitTx( - api.tx.StorageProvider.accept_agreement({ bucket_id: bucketId }), - provider.signer, - "accept_agreement" - ); + return event.bucket_id; } export async function setMember(api, admin, bucketId, member, role) { diff --git a/examples/papi/full-flow.js b/examples/papi/full-flow.js index 7e161170..f6339430 100644 --- a/examples/papi/full-flow.js +++ b/examples/papi/full-flow.js @@ -18,15 +18,14 @@ import assert from "node:assert"; import { - acceptAgreement, challengeCheckpoint, challengeOffchain, - createBucket, downloadChunk, endAgreement, + establishStorageAgreement, fetchChallengeProof, fetchCheckpointSignature, - requestPrimaryAgreement, + negotiateTerms, respondToChallenge, submitClientCheckpoint, uploadChunk, @@ -49,30 +48,35 @@ const { clientSeed: CLIENT_SEED, } = parseProviderClientArgs(); -async function setupAgreement(api, client, provider, bucketId) { - const existing = await api.query.StorageProvider.StorageAgreements.getValue( - bucketId, - provider.address - ); - if (existing) { - console.log(" Agreement already exists"); - return; - } +/** + * Negotiate provider-signed terms over HTTP, then redeem them on-chain. + * The bucket + primary agreement are opened atomically inside + * `establish_storage_agreement` — no separate create_bucket step. + */ +async function setupAgreement(api, providerUrl, client, provider) { const maxBytes = 1_073_741_824n; // 1 GiB const duration = 50; console.log( - " Requesting agreement (%s), duration=%d blocks...", - client.seed, + " Negotiating signed terms with provider (max_bytes=%s, duration=%d)...", + maxBytes, duration ); - await requestPrimaryAgreement(api, client, provider, bucketId, { - max_bytes: maxBytes, + const signed = await negotiateTerms(providerUrl, { + owner: client.address, + max_bytes: Number(maxBytes), duration, - max_payment: maxBytes * BigInt(duration) * 2n, + price_per_byte: 1, + replica_params: null, }); - console.log(" Accepting agreement (%s)...", provider.seed); - await acceptAgreement(api, provider, bucketId); - console.log(" Agreement accepted"); + console.log( + " Provider signed terms: nonce=%s, valid_until=%s", + signed.terms.nonce, + signed.terms.valid_until + ); + console.log(" Redeeming on-chain via establish_storage_agreement..."); + const bucketId = await establishStorageAgreement(api, client, provider, signed); + console.log(" Bucket %s opened with primary agreement", bucketId); + return bucketId; } async function uploadAndVerify(bucketId) { @@ -153,9 +157,7 @@ async function main() { try { console.log("\n=== Step 1: Setup ==="); await ensureProviderRegistered(api, provider, PROVIDER_URL); - const bucketId = await createBucket(api, client); - console.log(" Bucket created with ID:", bucketId); - await setupAgreement(api, client, provider, bucketId); + const bucketId = await setupAgreement(api, PROVIDER_URL, client, provider); console.log("\n=== Step 2: Upload data ==="); const upload = await uploadAndVerify(bucketId); diff --git a/justfile b/justfile index 63f864cb..a9c01cc2 100644 --- a/justfile +++ b/justfile @@ -144,8 +144,6 @@ start-provider MODE="inmemory" PORT=PROVIDER_PORT STORAGE_PATH="./provider-data" --storage-mode "{{MODE}}" \ --bind-addr "0.0.0.0:{{PORT}}" \ --chain-rpc "{{ CHAIN_WS }}" \ - --enable-agreement-coordinator \ - --enable-checkpoint-coordinator \ $EXTRA_ARGS # Register provider on-chain (idempotent). Requires a running chain. From 4134f392f167df140f0baa3938f79df9ae47dcc5 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 29 May 2026 17:15:53 +0700 Subject: [PATCH 14/44] chore: fmt --- client/src/admin.rs | 2 +- client/src/provider.rs | 5 ++--- client/tests/admin_integration.rs | 4 ++-- client/tests/common/mod.rs | 4 +--- pallet/src/mock.rs | 2 ++ pallet/src/tests.rs | 13 +++++++------ primitives/src/agreement_term.rs | 2 +- primitives/src/provider_replay_state.rs | 8 +++----- runtimes/web3-storage-paseo/tests/tests.rs | 2 +- .../file-system/pallet-registry/Cargo.toml | 1 + storage-interfaces/s3/pallet-s3-registry/Cargo.toml | 1 + .../s3/pallet-s3-registry/src/mock.rs | 2 ++ 12 files changed, 24 insertions(+), 22 deletions(-) diff --git a/client/src/admin.rs b/client/src/admin.rs index 450ce4e2..a1761d5c 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -1,7 +1,7 @@ //! Admin Client - For bucket administrators managing buckets and agreements. //! //! This client provides operations for: -//! - Redeeming provider-signed terms to open a bucket + primary agreement +//! - Establish storage agreement //! - Managing bucket members and permissions //! - Extending / topping up / terminating agreements //! - Freezing buckets diff --git a/client/src/provider.rs b/client/src/provider.rs index bbad7562..9536d49a 100644 --- a/client/src/provider.rs +++ b/client/src/provider.rs @@ -341,9 +341,8 @@ impl ProviderClient { /// Negotiate provider-signed agreement terms over HTTP. /// - /// Owner posts the proposed shape; the provider node allocates nonce - /// + validity window from its own state, signs, returns - /// [`SignedTerms`] ready for + /// Owner posts the proposed shape; the provider node allocates nonce + validity window from + /// its own state, signs, returns a [`SignedTerms`](crate::agreement::SignedTerms) ready for /// [`AdminClient::establish_storage_agreement`](crate::admin::AdminClient::establish_storage_agreement). pub async fn negotiate_terms( provider_url: &str, diff --git a/client/tests/admin_integration.rs b/client/tests/admin_integration.rs index 5e5538b3..743471cf 100644 --- a/client/tests/admin_integration.rs +++ b/client/tests/admin_integration.rs @@ -18,7 +18,7 @@ use storage_primitives::Role; /// Full bucket management lifecycle in dependency order: /// -/// chain_setup (registers Alice as provider + signs terms + opens bucket) +/// chain_setup (registers Alice as provider + signs terms + creates bucket) /// → get_bucket_info (verify initial state) /// → add_member (add Bob as Writer) /// → get_bucket_info (verify Bob present) @@ -35,7 +35,7 @@ async fn test_bucket_lifecycle() { // `chain_setup` registers Alice as a provider, signs primary terms with // her keypair, and redeems them via `establish_storage_agreement` to - // open a fresh bucket. Returns `None` when the chain isn't reachable. + // create a fresh bucket. Returns `None` when the chain isn't reachable. let setup = match common::chain_setup().await { Some(s) => s, None => { diff --git a/client/tests/common/mod.rs b/client/tests/common/mod.rs index 19e584ec..d88fca6d 100644 --- a/client/tests/common/mod.rs +++ b/client/tests/common/mod.rs @@ -126,8 +126,6 @@ pub async fn chain_setup() -> Option { if !already_registered { // Idempotent: ignore "already registered" errors so tests survive races. - // We register with Alice's real sr25519 pubkey so she can sign her own - // terms below for the establish_storage_agreement flow. let _ = provider .register( "/ip4/127.0.0.1/tcp/3333".to_string(), @@ -138,7 +136,7 @@ pub async fn chain_setup() -> Option { let _ = provider .update_settings(ProviderSettings { - price_per_byte: 0, + price_per_byte: 1_000_000, min_duration: 1, max_duration: 1_000_000, accepting_primary: true, diff --git a/pallet/src/mock.rs b/pallet/src/mock.rs index 53b1b247..734a1d8c 100644 --- a/pallet/src/mock.rs +++ b/pallet/src/mock.rs @@ -120,6 +120,8 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .unwrap(); let mut ext: sp_io::TestExternalities = t.into(); + // Required so signed-terms tests can call `sr25519_sign` through the + // keystore extension. ext.register_extension(sp_keystore::KeystoreExt::new( sp_keystore::testing::MemoryKeystore::new(), )); diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index a3e8717c..e824f25f 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -65,6 +65,7 @@ fn primary_terms( } /// Build [`AgreementTermsOf`] for a replica agreement. +#[allow(clippy::too_many_arguments)] fn replica_terms( owner: u64, max_bytes: u64, @@ -940,7 +941,7 @@ mod establish_storage_agreement_tests { assert_eq!(Balances::free_balance(1), owner_balance_before); // Replay window now anchored at nonce 7. - let window = ProviderReplayState::::get(2); + let window = ProviderReplayStates::::get(2); assert_eq!(window.hwm, 7); assert_eq!(window.bitmap[0] & 1, 1); }); @@ -1147,7 +1148,7 @@ mod establish_storage_agreement_tests { fn rejects_when_signed_price_below_on_chain_price() { // If a provider raises their on-chain price after signing, the // pallet enforces `provider_info.price_per_byte <= terms.price_per_byte` - // and rejects with `PriceExceedsMax`. + // and rejects with `PaymentExceedsMax`. new_test_ext().execute_with(|| { System::set_block_number(1); let settings = ProviderSettings { @@ -1171,7 +1172,7 @@ mod establish_storage_agreement_tests { terms, sig, ), - Error::::PriceExceedsMax + Error::::PaymentExceedsMax ); }); } @@ -1246,7 +1247,7 @@ mod establish_storage_agreement_tests { advance, sig, )); - assert_eq!(ProviderReplayState::::get(2).hwm, 300); + assert_eq!(ProviderReplayStates::::get(2).hwm, 300); // Distance == REPLAY_WINDOW_BITS - 1 ⇒ accepted. let edge_nonce = 300 - (REPLAY_WINDOW_BITS as u64 - 1); @@ -1326,7 +1327,7 @@ mod establish_storage_agreement_tests { } // hwm follows the max nonce seen. - let window = ProviderReplayState::::get(2); + let window = ProviderReplayStates::::get(2); assert_eq!(window.hwm, 10); // Replays of any of those nonces are rejected. @@ -1376,7 +1377,7 @@ mod establish_storage_agreement_tests { sig, )); - let window = ProviderReplayState::::get(2); + let window = ProviderReplayStates::::get(2); assert_eq!(window.hwm, 10_000); // Only the new hwm bit is set; everything else is zero. assert_eq!(window.bitmap[0], 0b0000_0001); diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs index f3a507fc..445dda4c 100644 --- a/primitives/src/agreement_term.rs +++ b/primitives/src/agreement_term.rs @@ -34,7 +34,7 @@ pub struct AgreementTerms { pub nonce: u64, /// Replica-specific parameters. /// - `None` means these are primary terms; - /// -`Some(_)` means the provider has quoted a replica agreement and the extra per-sync funding is included. + /// - `Some(_)` means the provider has quoted a replica agreement and the extra per-sync funding is included. pub replica_params: Option>, } diff --git a/primitives/src/provider_replay_state.rs b/primitives/src/provider_replay_state.rs index 06c7e801..80bf2fa3 100644 --- a/primitives/src/provider_replay_state.rs +++ b/primitives/src/provider_replay_state.rs @@ -106,11 +106,9 @@ fn shift_left_le(bytes: &mut [u8; 32], shift: u64) { let mut out = [0u8; 32]; if bit_shift == 0 { - for i in byte_shift..32 { - out[i] = bytes[i - byte_shift]; - } + out[byte_shift..32].copy_from_slice(&bytes[..(32 - byte_shift)]); } else { - for i in byte_shift..32 { + for (i, slot) in out.iter_mut().enumerate().skip(byte_shift) { let src = i - byte_shift; let lo = bytes[src] << bit_shift; let hi = if src > 0 { @@ -118,7 +116,7 @@ fn shift_left_le(bytes: &mut [u8; 32], shift: u64) { } else { 0 }; - out[i] = lo | hi; + *slot = lo | hi; } } *bytes = out; diff --git a/runtimes/web3-storage-paseo/tests/tests.rs b/runtimes/web3-storage-paseo/tests/tests.rs index 145f64f9..d9edc36d 100644 --- a/runtimes/web3-storage-paseo/tests/tests.rs +++ b/runtimes/web3-storage-paseo/tests/tests.rs @@ -145,7 +145,7 @@ fn primary_terms( AgreementTerms { owner, max_bytes, - duration: duration.into(), + duration, price_per_byte: 0, valid_until: 1_000_000_000, nonce, diff --git a/storage-interfaces/file-system/pallet-registry/Cargo.toml b/storage-interfaces/file-system/pallet-registry/Cargo.toml index 5bd2d460..17966ade 100644 --- a/storage-interfaces/file-system/pallet-registry/Cargo.toml +++ b/storage-interfaces/file-system/pallet-registry/Cargo.toml @@ -39,6 +39,7 @@ std = [ "scale-info/std", "sp-core/std", "sp-io/std", + "sp-keystore/std", "sp-runtime/std", "storage-primitives/std", ] diff --git a/storage-interfaces/s3/pallet-s3-registry/Cargo.toml b/storage-interfaces/s3/pallet-s3-registry/Cargo.toml index a1fc0e60..cd009baf 100644 --- a/storage-interfaces/s3/pallet-s3-registry/Cargo.toml +++ b/storage-interfaces/s3/pallet-s3-registry/Cargo.toml @@ -45,6 +45,7 @@ std = [ "scale-info/std", "sp-core/std", "sp-io?/std", + "sp-keystore/std", "sp-runtime/std", "storage-primitives/std", ] diff --git a/storage-interfaces/s3/pallet-s3-registry/src/mock.rs b/storage-interfaces/s3/pallet-s3-registry/src/mock.rs index b20dc878..31cbcac0 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/mock.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/mock.rs @@ -133,6 +133,8 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .unwrap(); let mut ext = sp_io::TestExternalities::new(t); + // Required so signed-terms tests can call `sr25519_sign` through the + // keystore extension. ext.register_extension(sp_keystore::KeystoreExt::new( sp_keystore::testing::MemoryKeystore::new(), )); From 00408becd2a6883eba24378aaf9cf3e898687f90 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 29 May 2026 17:27:41 +0700 Subject: [PATCH 15/44] chore: fmt --- client/src/admin.rs | 3 --- client/src/agreement.rs | 10 +++------- client/src/substrate.rs | 6 ------ 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/client/src/admin.rs b/client/src/admin.rs index a1761d5c..b3544c0a 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -58,9 +58,6 @@ impl AdminClient { /// [`ProviderClient::negotiate_terms`](crate::provider::ProviderClient::negotiate_terms), /// but any source that produces a valid signature works. /// - /// On success, returns the new bucket id (parsed from the - /// `BucketCreated` event emitted by the runtime). - /// /// # Example /// ```no_run /// # use storage_client::{AdminClient, NegotiateRequest, ProviderClient}; diff --git a/client/src/agreement.rs b/client/src/agreement.rs index c86e4eab..1c3eda6b 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -1,10 +1,6 @@ //! Provider-signed agreement terms — wire format + client-side signing helper. //! -//! Bucket creation went from a request/accept handshake to a single -//! `establish_storage_agreement` redemption of provider-signed -//! [`AgreementTerms`]. This module holds: -//! -//! * The runtime-specific [`AgreementTermsOf`] alias the client uses to +//! * Runtime-specific [`AgreementTermsOf`] alias the client uses to //! talk to the parachain (AccountId32 / u128 / u32). //! * [`SignedTerms`] — the negotiated bundle returned over HTTP by a //! provider node's `/negotiate` endpoint. @@ -26,8 +22,8 @@ use storage_primitives::AgreementTerms; /// Concrete [`AgreementTerms`] type for the storage parachain. /// -/// Balance is `u128`, BlockNumber is `u32`; matches `parachains_common`'s -/// runtime types used by runtime. +/// Balance is `u128`, BlockNumber is `u32`; matches +/// types used by runtime. pub type AgreementTermsOf = AgreementTerms; /// Concrete `ReplicaTerms` matching the parachain's diff --git a/client/src/substrate.rs b/client/src/substrate.rs index d0288d29..9210807d 100644 --- a/client/src/substrate.rs +++ b/client/src/substrate.rs @@ -851,12 +851,6 @@ pub mod storage { } /// Query the provider's replay-window state. - /// - /// Returns the storage address; the caller fetches it through subxt - /// and decodes the `ReplayWindow { hwm, bitmap }` payload. The - /// `hwm` field is what the provider node's nonce counter bootstraps - /// from on cold start so reissued nonces would land outside the - /// replay window and be rejected by Layer 0. pub fn provider_replay_state( account: &AccountId32, ) -> subxt::storage::DefaultAddress< From e36601cd46af13507264947dfb4406e6b86390f8 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:01:45 +0700 Subject: [PATCH 16/44] feat: adopt storage-interfaces/file-system to new agreement flow --- .github/workflows/integration-tests.yml | 2 - client/examples/register_provider.rs | 16 +-- client/src/provider.rs | 70 +++++++++-- client/src/substrate.rs | 67 ++++++----- provider-node/src/api.rs | 5 +- provider-node/src/command.rs | 110 +++++------------- provider-node/src/types.rs | 4 +- .../client/examples/basic_usage.rs | 91 +++++++++------ .../client/examples/ci_integration_test.rs | 68 +++++++---- .../file-system/client/src/lib.rs | 64 +++++----- .../file-system/client/src/substrate.rs | 36 +++--- 11 files changed, 281 insertions(+), 252 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index fc1dc567..231be53b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -145,7 +145,6 @@ jobs: nohup ./target/release/storage-provider-node \ --keyfile /tmp/alice-key --storage-mode inmemory \ --bind-addr 0.0.0.0:3333 --chain-rpc ws://127.0.0.1:2222 \ - --enable-agreement-coordinator --enable-checkpoint-coordinator \ > /tmp/provider.log 2>&1 & echo "Waiting for provider HTTP server..." for i in $(seq 1 60); do @@ -167,7 +166,6 @@ jobs: nohup ./target/release/storage-provider-node \ --keyfile /tmp/charlie-key --storage-mode disk --storage-path /tmp/provider-data \ --bind-addr 0.0.0.0:3334 --chain-rpc ws://127.0.0.1:2222 \ - --enable-agreement-coordinator --enable-checkpoint-coordinator \ > /tmp/provider-disk.log 2>&1 & echo "Waiting for disk provider HTTP server..." for i in $(seq 1 60); do diff --git a/client/examples/register_provider.rs b/client/examples/register_provider.rs index 0eb2b12a..9c76df4b 100644 --- a/client/examples/register_provider.rs +++ b/client/examples/register_provider.rs @@ -17,23 +17,25 @@ use std::env; use storage_client::{ClientConfig, ProviderClient, ProviderSettings}; use subxt_signer::{sr25519::Keypair, SecretUri}; +const DEFAULT_CHAIN_WS: &str = "ws://127.0.0.1:2222"; +const DEFAULT_PROVIDER_URL: &str = "http://127.0.0.1:3333"; +const DEFAULT_PROVIDER_MULTIADDR: &str = "/ip4/127.0.0.1/tcp/3333"; +const DEFAULT_KEYRING: &str = "//Alice"; + #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let args: Vec = env::args().collect(); - let chain_ws = args - .get(1) - .map(String::as_str) - .unwrap_or("ws://127.0.0.1:2222"); + let chain_ws = args.get(1).map(String::as_str).unwrap_or(DEFAULT_CHAIN_WS); let provider_url = args .get(2) .map(String::as_str) - .unwrap_or("http://127.0.0.1:3333"); + .unwrap_or(DEFAULT_PROVIDER_URL); let multiaddr = args .get(3) .cloned() - .unwrap_or_else(|| "/ip4/127.0.0.1/tcp/3333".to_string()); + .unwrap_or_else(|| DEFAULT_PROVIDER_MULTIADDR.to_string()); // Load keypair from keyfile seed (e.g. "//Alice") or fall back to //Alice. // The keyfile format matches start-provider: a single seed phrase per line. @@ -43,7 +45,7 @@ async fn main() -> Result<(), Box> { let uri: SecretUri = seed.trim().parse()?; Keypair::from_uri(&uri)? } else { - Keypair::from_uri(&"//Alice".parse::()?)? + Keypair::from_uri(&DEFAULT_KEYRING.parse::()?)? }; // Derive SS58 address from the keypair for display and ProviderClient identity. diff --git a/client/src/provider.rs b/client/src/provider.rs index 9536d49a..f5a075a7 100644 --- a/client/src/provider.rs +++ b/client/src/provider.rs @@ -144,16 +144,7 @@ impl ProviderClient { // Decode top-level fields. let multiaddr = named_field(&value, "multiaddr") - .map(|v| match &v.value { - ValueDef::Composite(Composite::Unnamed(items)) => { - let bytes: Vec = items - .iter() - .filter_map(|b| b.as_u128().map(|n| n as u8)) - .collect(); - String::from_utf8_lossy(&bytes).into_owned() - } - _ => String::new(), - }) + .map(|v| String::from_utf8_lossy(&decode_byte_vec(v)).into_owned()) .unwrap_or_default(); let stake = named_field(&value, "stake") @@ -369,6 +360,40 @@ impl ProviderClient { .map_err(ClientError::Http) } + /// Fetch a provider node's identity from its `/info` HTTP endpoint. + /// + /// Returns the provider's account as an [`AccountId32`], parsed from the + /// SS58 string the node reports. Useful for discovering the provider's + /// on-chain account without hardcoding it. + pub async fn fetch_provider_id(provider_url: &str) -> ClientResult { + let url = format!("{}/info", provider_url.trim_end_matches('/')); + let response = reqwest::Client::new() + .get(&url) + .send() + .await + .map_err(ClientError::Http)?; + + if !response.status().is_success() { + return Err(ClientError::Chain(format!( + "provider node rejected /info with status {}", + response.status() + ))); + } + + #[derive(serde::Deserialize)] + struct InfoResponse { + provider_id: String, + } + + let info = response + .json::() + .await + .map_err(ClientError::Http)?; + + SubstrateClient::parse_account(&info.provider_id) + .map_err(|e| ClientError::Chain(format!("invalid provider_id from /info: {e}"))) + } + /// List all active agreements for this provider. pub async fn list_active_agreements(&self) -> ClientResult> { let chain = self.base.chain()?; @@ -782,6 +807,31 @@ fn decode_account_bytes(value: &subxt::ext::scale_value::Value) -> Option` / `BoundedVec` from a scale_value composite. +/// +/// `BoundedVec` serializes its `TypeInfo` as a 1-field unnamed composite +/// wrapping the inner `Vec`, so scale_value surfaces it as +/// `Composite::Unnamed([inner_vec])`. This helper drills through that wrapper +/// if present, then collects the bytes. +fn decode_byte_vec(value: &subxt::ext::scale_value::Value) -> Vec { + let ValueDef::Composite(Composite::Unnamed(items)) = &value.value else { + return Vec::new(); + }; + // Direct sequence of byte primitives. + let bytes: Vec = items + .iter() + .filter_map(|b| b.as_u128().map(|n| n as u8)) + .collect(); + if !items.is_empty() && bytes.len() == items.len() { + return bytes; + } + // BoundedVec wrapper: single inner field holds the actual sequence. + if items.len() == 1 { + return decode_byte_vec(&items[0]); + } + Vec::new() +} + // Types #[derive(Debug, Clone)] diff --git a/client/src/substrate.rs b/client/src/substrate.rs index 9210807d..221690f8 100644 --- a/client/src/substrate.rs +++ b/client/src/substrate.rs @@ -4,6 +4,7 @@ //! the storage parachain. use crate::base::ClientError; +use codec::Encode; use futures::StreamExt; use sp_core::H256; use sp_runtime::AccountId32; @@ -184,18 +185,11 @@ pub mod extrinsics { ) } - /// Build an `establish_storage_agreement` extrinsic payload. - /// - /// Bundles the SCALE-encoded provider-signed terms and signature into - /// the dynamic call shape Layer 0 expects. The chain hashes - /// `blake2_256(SCALE(terms))` and verifies the signature against the - /// provider's registered public key. - pub fn establish_storage_agreement( - provider: AccountId32, + /// Encode an [`AgreementTermsOf`](crate::agreement::AgreementTermsOf) as + /// a subxt dynamic value matching the on-chain composite. + pub fn dynamic_agreement_terms( terms: &crate::agreement::AgreementTermsOf, - sig: &sp_runtime::MultiSignature, - ) -> impl Payload { - use codec::Encode; + ) -> subxt::dynamic::Value { let replica_params_value = match &terms.replica_params { None => subxt::dynamic::Value::unnamed_variant("None", vec![]), Some(rp) => subxt::dynamic::Value::unnamed_variant( @@ -209,7 +203,7 @@ pub mod extrinsics { ])], ), }; - let terms_value = subxt::dynamic::Value::named_composite([ + subxt::dynamic::Value::named_composite([ ( "owner", subxt::dynamic::Value::from_bytes(terms.owner.as_ref() as &[u8]), @@ -232,36 +226,41 @@ pub mod extrinsics { ), ("nonce", subxt::dynamic::Value::u128(terms.nonce as u128)), ("replica_params", replica_params_value), - ]); + ]) + } - // MultiSignature is a SCALE enum; surface it as `Sr25519` for the - // current signer (the only variant the provider node emits today). - let sig_value = match sig { - sp_runtime::MultiSignature::Sr25519(s) => subxt::dynamic::Value::unnamed_variant( - "Sr25519", - vec![subxt::dynamic::Value::from_bytes(s.encode())], - ), - sp_runtime::MultiSignature::Ed25519(s) => subxt::dynamic::Value::unnamed_variant( - "Ed25519", - vec![subxt::dynamic::Value::from_bytes(s.encode())], - ), - sp_runtime::MultiSignature::Ecdsa(s) => subxt::dynamic::Value::unnamed_variant( - "Ecdsa", - vec![subxt::dynamic::Value::from_bytes(s.encode())], - ), - sp_runtime::MultiSignature::Eth(s) => subxt::dynamic::Value::unnamed_variant( - "Eth", - vec![subxt::dynamic::Value::from_bytes(s.encode())], - ), + /// Encode a [`sp_runtime::MultiSignature`] as a subxt dynamic variant. + pub fn dynamic_multi_signature(sig: &sp_runtime::MultiSignature) -> subxt::dynamic::Value { + let (variant, bytes) = match sig { + sp_runtime::MultiSignature::Sr25519(s) => ("Sr25519", s.encode()), + sp_runtime::MultiSignature::Ed25519(s) => ("Ed25519", s.encode()), + sp_runtime::MultiSignature::Ecdsa(s) => ("Ecdsa", s.encode()), + sp_runtime::MultiSignature::Eth(s) => ("Eth", s.encode()), }; + subxt::dynamic::Value::unnamed_variant( + variant, + vec![subxt::dynamic::Value::from_bytes(bytes)], + ) + } + /// Build an `establish_storage_agreement` extrinsic payload. + /// + /// Bundles the SCALE-encoded provider-signed terms and signature into + /// the dynamic call shape Layer 0 expects. The chain hashes + /// `blake2_256(SCALE(terms))` and verifies the signature against the + /// provider's registered public key. + pub fn establish_storage_agreement( + provider: AccountId32, + terms: &crate::agreement::AgreementTermsOf, + sig: &sp_runtime::MultiSignature, + ) -> impl Payload { subxt::dynamic::tx( PALLET_NAME, "establish_storage_agreement", vec![ subxt::dynamic::Value::from_bytes(provider.as_ref() as &[u8]), - terms_value, - sig_value, + dynamic_agreement_terms(terms), + dynamic_multi_signature(sig), ], ) } diff --git a/provider-node/src/api.rs b/provider-node/src/api.rs index 7ff1f259..455f3c29 100644 --- a/provider-node/src/api.rs +++ b/provider-node/src/api.rs @@ -122,10 +122,9 @@ async fn health() -> Json { }) } -async fn info() -> Json { +async fn info(State(state): State>) -> Json { Json(InfoResponse { - status: "healthy".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), + provider_id: state.provider_id.clone(), }) } diff --git a/provider-node/src/command.rs b/provider-node/src/command.rs index 9bf8d27d..8c7b3db6 100644 --- a/provider-node/src/command.rs +++ b/provider-node/src/command.rs @@ -11,7 +11,6 @@ use clap::Parser; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use subxt::dynamic::At; use subxt::{dynamic::Value, OnlineClient, PolkadotConfig}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -227,8 +226,10 @@ async fn setup_nonce_counter( .map_err(|e| format!("failed to open nonce counter at {:?}: {}", nonce_path, e))? } }; + // Start the `nonce` from 1. + counter.bootstrap_from_hwm(0); - // Bootstrap from on-chain hwm. Best-effort: if the chain isn't + // Otherwise, bootstrap from on-chain hwm. Best-effort: if the chain isn't // reachable yet, fall back to the local counter — the on-chain // replay window will reject any out-of-range reissues anyway. let provider_account = sp_runtime::AccountId32::from_str(provider_id) @@ -279,41 +280,34 @@ fn bind_addr_to_multiaddr(bind_addr: &str) -> String { async fn sync_multiaddr_on_chain(chain_rpc: &str, seed: &str, provider_id: &str, bind_addr: &str) { let expected_multiaddr = bind_addr_to_multiaddr(bind_addr); - let api = match OnlineClient::::from_url(chain_rpc).await { - Ok(api) => api, - Err(e) => { - tracing::warn!("Could not connect to chain for multiaddr sync: {}", e); + let account = match sp_runtime::AccountId32::from_str(provider_id) { + Ok(a) => a, + Err(_) => { + tracing::warn!("Invalid provider SS58 address, skipping multiaddr sync"); return; } }; - // Read current on-chain provider info - let our_account: sp_core::crypto::AccountId32 = - match sp_core::crypto::Ss58Codec::from_ss58check(provider_id) { - Ok(a) => a, - Err(_) => { - tracing::warn!("Invalid provider SS58 address, skipping multiaddr sync"); - return; - } - }; - let our_bytes: [u8; 32] = our_account.into(); - - let storage_query = subxt::dynamic::storage( - "StorageProvider", - "Providers", - vec![Value::from_bytes(our_bytes)], - ); - - let result = match api.storage().at_latest().await { - Ok(s) => s.fetch(&storage_query).await, + let mut client = match storage_client::ProviderClient::new( + storage_client::ClientConfig { + chain_ws_url: chain_rpc.to_string(), + ..Default::default() + }, + provider_id.to_string(), + ) { + Ok(c) => c, Err(e) => { - tracing::warn!("Failed to query storage for multiaddr sync: {}", e); + tracing::warn!("Could not build provider client for multiaddr sync: {}", e); return; } }; + if let Err(e) = client.connect().await { + tracing::warn!("Could not connect to chain for multiaddr sync: {}", e); + return; + } - let provider_value = match result { - Ok(Some(v)) => v, + let current = match client.get_provider_info(&account).await { + Ok(Some(info)) => info.multiaddr, Ok(None) => { tracing::info!("Provider not registered on chain yet, skipping multiaddr sync"); return; @@ -324,58 +318,6 @@ async fn sync_multiaddr_on_chain(chain_rpc: &str, seed: &str, provider_id: &str, } }; - // Extract multiaddr from the encoded provider storage entry. - // Decode to scale_value and extract the "multiaddr" field bytes. - let current = { - let decoded = match provider_value.to_value() { - Ok(v) => v, - Err(e) => { - tracing::warn!("Could not decode provider value: {}, skipping sync", e); - return; - } - }; - - // Use At trait to navigate the decoded value - let multiaddr_val = match decoded.at("multiaddr") { - Some(v) => v, - None => { - tracing::warn!("No multiaddr field in provider info, skipping sync"); - return; - } - }; - - // Extract bytes from the value — could be a sequence of u8 values - fn extract_bytes(val: &subxt::ext::scale_value::Value) -> Vec { - use subxt::ext::scale_value::{Composite, Primitive, ValueDef}; - match &val.value { - // Sequence/composite of individual byte values - ValueDef::Composite(Composite::Unnamed(items)) => items - .iter() - .filter_map(|item| match &item.value { - ValueDef::Primitive(Primitive::U128(n)) => Some(*n as u8), - _ => None, - }) - .collect(), - // Some subxt versions encode Vec as a Primitive::U256 or similar - ValueDef::Primitive(Primitive::U128(n)) => { - // Single byte - vec![*n as u8] - } - _ => vec![], - } - } - - let bytes = extract_bytes(multiaddr_val); - if bytes.is_empty() { - // Debug: log the actual value structure to understand the encoding - tracing::warn!( - "multiaddr decoded as empty, value structure: {:?}", - multiaddr_val - ); - } - String::from_utf8_lossy(&bytes).to_string() - }; - if current == expected_multiaddr { tracing::info!( "On-chain multiaddr matches bind address: {}", @@ -390,6 +332,14 @@ async fn sync_multiaddr_on_chain(chain_rpc: &str, seed: &str, provider_id: &str, expected_multiaddr ); + let api = match OnlineClient::::from_url(chain_rpc).await { + Ok(api) => api, + Err(e) => { + tracing::warn!("Could not connect to chain for multiaddr update: {}", e); + return; + } + }; + // Create signer from seed let uri: subxt_signer::SecretUri = match seed.parse() { Ok(u) => u, diff --git a/provider-node/src/types.rs b/provider-node/src/types.rs index da07dd54..c69c2219 100644 --- a/provider-node/src/types.rs +++ b/provider-node/src/types.rs @@ -245,8 +245,8 @@ pub struct ListBucketsResponse { /// Provider info response. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InfoResponse { - pub status: String, - pub version: String, + pub provider_id: String, + // TODO: Add more provider information } /// Health check response. diff --git a/storage-interfaces/file-system/client/examples/basic_usage.rs b/storage-interfaces/file-system/client/examples/basic_usage.rs index d4e4c957..bb5380fe 100644 --- a/storage-interfaces/file-system/client/examples/basic_usage.rs +++ b/storage-interfaces/file-system/client/examples/basic_usage.rs @@ -1,7 +1,8 @@ //! Basic Usage Example for File System Client //! //! This example demonstrates: -//! - Creating a drive with storage infrastructure +//! - Negotiating signed agreement terms with a provider +//! - Creating a drive (atomically opens bucket + primary agreement) //! - Creating directories //! - Uploading files //! - Listing directory contents @@ -10,8 +11,8 @@ //! Prerequisites: //! 1. Start the blockchain: `just start-chain` //! 2. Start a provider node: `just start-provider` -//! 3. With both running, run `just demo` once to register the provider and -//! open an agreement (or do the equivalent steps manually in Polkadot.js). +//! 3. Provider must be registered with `accepting_primary = true` (run `just demo` +//! once or register manually via Polkadot.js). //! //! Run this example: //! ```bash @@ -19,10 +20,15 @@ //! ``` use file_system_client::FileSystemClient; +use sp_runtime::AccountId32; +use storage_client::{NegotiateRequest, ProviderClient}; +use subxt_signer::sr25519::dev as dev_signer; + +const CHAIN_WS: &str = "ws://127.0.0.1:2222"; +const PROVIDER_URL: &str = "http://127.0.0.1:3333"; #[tokio::main] async fn main() -> Result<(), Box> { - // Initialize logging tracing_subscriber::fmt::init(); println!("🚀 File System Client - Basic Usage Example\n"); @@ -31,69 +37,78 @@ async fn main() -> Result<(), Box> { // === STEP 1: Create the client === println!("\n📡 Step 1: Connecting to blockchain and provider..."); - let mut fs_client = FileSystemClient::new( - "ws://127.0.0.1:2222", // Parachain WebSocket endpoint - "http://127.0.0.1:3333", // Provider HTTP endpoint + let mut fs_client = FileSystemClient::new(CHAIN_WS, PROVIDER_URL) + .await? + .with_dev_signer("alice") // Use Alice for testing + .await?; + + let owner: AccountId32 = dev_signer::alice().public_key().0.into(); + let provider = ProviderClient::fetch_provider_id(PROVIDER_URL).await?; + println!(" Provider account (from /info): {provider}"); + + println!("✅ Connected successfully!"); + + // === STEP 2: Negotiate signed terms with the provider === + println!("\n🤝 Step 2: Negotiating signed agreement terms..."); + + let signed = ProviderClient::negotiate_terms( + PROVIDER_URL, + &NegotiateRequest { + owner: owner.clone(), + max_bytes: 10_000_000_000, // 10 GB + duration: 500, // 500 blocks + price_per_byte: 1, + replica_params: None, + }, ) - .await? - .with_dev_signer("alice") // Use Alice for testing .await?; - println!("✅ Connected successfully!"); + println!( + "✅ Provider signed terms: nonce={}, valid_until={}", + signed.terms.nonce, signed.terms.valid_until + ); - // === STEP 2: Create a drive === - println!("\n📁 Step 2: Creating a new drive..."); + // === STEP 3: Create a drive === + println!("\n📁 Step 3: Creating a new drive..."); let drive_id = fs_client .create_drive( - Some("My Documents"), // Drive name - 10_000_000_000, // 10 GB capacity - 500, // 500 blocks duration - 1_000_000_000_000, // 1 token payment (12 decimals) - None, // Auto-determine providers + Some("My Documents"), + provider, + signed.terms, + signed.signature, ) .await?; println!("✅ Drive created with ID: {drive_id}"); - println!(" Name: My Documents"); - println!(" Capacity: 10 GB"); - println!(" Duration: 500 blocks"); - - // Note: In a real scenario, you'd need to wait for bucket creation and agreement setup - // For this example, we'll assume that's done (via manual setup or scripts) - // === STEP 3: Create directories === - println!("\n📂 Step 3: Creating directory structure..."); + // === STEP 4: Create directories === + println!("\n📂 Step 4: Creating directory structure..."); - // Get bucket_id from drive (you'd normally query this from chain) - // For now, we use a placeholder - let bucket_id = 1u64; // This should come from the drive info + let bucket_id = fs_client.get_bucket_id(drive_id).await?; + println!(" Associated bucket: ID = {bucket_id}"); - // Create /documents directory println!(" Creating /documents..."); fs_client .create_directory(drive_id, "/documents", bucket_id) .await?; println!(" ✅ Created /documents"); - // Create /documents/work subdirectory println!(" Creating /documents/work..."); fs_client .create_directory(drive_id, "/documents/work", bucket_id) .await?; println!(" ✅ Created /documents/work"); - // Create /photos directory println!(" Creating /photos..."); fs_client .create_directory(drive_id, "/photos", bucket_id) .await?; println!(" ✅ Created /photos"); - // === STEP 4: Upload files === - println!("\n📝 Step 4: Uploading files..."); + // === STEP 5: Upload files === + println!("\n📝 Step 5: Uploading files..."); - // Upload a text file let readme_content = b"# My Documents\n\nWelcome to my decentralized file system!\n\nThis is a demo of Layer 1 file system built on Scalable Web3 Storage."; println!( " Uploading /README.md ({} bytes)...", @@ -132,8 +147,8 @@ async fn main() -> Result<(), Box> { .await?; println!(" ✅ Uploaded /documents/notes.txt"); - // === STEP 5: List directory contents === - println!("\n📋 Step 5: Listing directory contents..."); + // === STEP 6: List directory contents === + println!("\n📋 Step 6: Listing directory contents..."); // List root directory println!("\n Contents of /:"); @@ -176,8 +191,8 @@ async fn main() -> Result<(), Box> { ); } - // === STEP 6: Download and verify files === - println!("\n⬇️ Step 6: Downloading and verifying files..."); + // === STEP 7: Download and verify files === + println!("\n⬇️ Step 7: Downloading and verifying files..."); // Download README.md println!("\n Downloading /README.md..."); diff --git a/storage-interfaces/file-system/client/examples/ci_integration_test.rs b/storage-interfaces/file-system/client/examples/ci_integration_test.rs index f9488ab7..a249357d 100644 --- a/storage-interfaces/file-system/client/examples/ci_integration_test.rs +++ b/storage-interfaces/file-system/client/examples/ci_integration_test.rs @@ -2,11 +2,12 @@ //! //! This test is designed to run in CI after the infrastructure is set up. //! It tests the full file system workflow: -//! 1. Create a drive (which creates bucket + agreement internally) -//! 2. Create directories -//! 3. Upload files -//! 4. List directories -//! 5. Download and verify files +//! 1. Negotiate signed agreement terms with the provider +//! 2. Create a drive (atomically opens bucket + primary agreement) +//! 3. Create directories +//! 4. Upload files +//! 5. List directories +//! 6. Download and verify files //! //! Usage: cargo run --example ci_integration_test //! @@ -14,7 +15,10 @@ use file_system_client::FileSystemClient; use file_system_primitives::DirectoryEntry; +use sp_runtime::AccountId32; use std::env; +use storage_client::{NegotiateRequest, ProviderClient}; +use subxt_signer::sr25519::dev as dev_signer; async fn list_and_verify( fs_client: &mut FileSystemClient, @@ -65,27 +69,51 @@ async fn main() -> Result<(), Box> { .await?; println!(" Client connected successfully"); - // Step 2: Create a drive + let owner: AccountId32 = dev_signer::alice().public_key().0.into(); + + // Discover the provider's on-chain account from its /info endpoint + // instead of hardcoding it. + let provider = ProviderClient::fetch_provider_id(provider_url).await?; + println!(" Provider account (from /info): {provider}"); + + // Step 2: Negotiate signed agreement terms + println!(); + println!("Step 2: Negotiating signed terms with provider..."); + let signed = ProviderClient::negotiate_terms( + provider_url, + &NegotiateRequest { + owner, + max_bytes: 1_000_000_000, // 1 GB + duration: 500, // 500 blocks + price_per_byte: 0, + replica_params: None, + }, + ) + .await?; + println!( + " Provider signed terms: nonce={}, valid_until={}", + signed.terms.nonce, signed.terms.valid_until + ); + + // Step 3: Create a drive println!(); - println!("Step 2: Creating drive..."); + println!("Step 3: Creating drive..."); let drive_id = fs_client .create_drive( Some("CI Test Drive"), - 1_000_000_000, // 1 GB capacity - 500, // 500 blocks duration - 1_000_000_000_000_000, // 1000 tokens payment (12 decimals) - Some(1), // 1 provider minimum + provider, + signed.terms, + signed.signature, ) .await?; println!(" Drive created: ID = {drive_id}"); - // Get the bucket_id for this drive let bucket_id = fs_client.get_bucket_id(drive_id).await?; println!(" Associated bucket: ID = {bucket_id}"); - // Step 3: Create directories + // Step 4: Create directories println!(); - println!("Step 3: Creating directories..."); + println!("Step 4: Creating directories..."); fs_client .create_directory(drive_id, "/test-dir", bucket_id) .await?; @@ -96,9 +124,9 @@ async fn main() -> Result<(), Box> { .await?; println!(" Created /test-dir/subdir"); - // Step 4: Upload files + // Step 5: Upload files println!(); - println!("Step 4: Uploading files..."); + println!("Step 5: Uploading files..."); let test_content_1 = b"Hello from CI integration test!"; fs_client @@ -123,9 +151,9 @@ async fn main() -> Result<(), Box> { test_content_2.len() ); - // Step 5: List directories + // Step 6: List directories println!(); - println!("Step 5: Listing directories..."); + println!("Step 6: Listing directories..."); let root_entries = list_and_verify(&mut fs_client, drive_id, "/", 1).await?; assert!( @@ -141,9 +169,9 @@ async fn main() -> Result<(), Box> { list_and_verify(&mut fs_client, drive_id, "/test-dir", 2).await?; list_and_verify(&mut fs_client, drive_id, "/test-dir/subdir", 1).await?; - // Step 6: Download and verify files + // Step 7: Download and verify files println!(); - println!("Step 6: Downloading and verifying files..."); + println!("Step 7: Downloading and verifying files..."); let downloaded_1 = fs_client .download_file(drive_id, "/test-dir/hello.txt") diff --git a/storage-interfaces/file-system/client/src/lib.rs b/storage-interfaces/file-system/client/src/lib.rs index 010051dd..2150c3f0 100644 --- a/storage-interfaces/file-system/client/src/lib.rs +++ b/storage-interfaces/file-system/client/src/lib.rs @@ -41,7 +41,7 @@ use file_system_primitives::{ compute_cid, Cid, DirectoryEntry, DirectoryNode, EntryType, FileManifest, }; use sp_core::H256; -use sp_runtime::BoundedVec; +use sp_runtime::{AccountId32, BoundedVec}; use std::collections::HashMap; use std::sync::Arc; use storage_client::{ @@ -168,46 +168,47 @@ impl FileSystemClient { /// # Arguments /// /// * `name` - Optional human-readable name for the drive - /// * `max_capacity` - Maximum storage capacity in bytes (e.g., 10 GB = 10_000_000_000) - /// * `storage_period` - Storage duration in blocks (e.g., 500 blocks) - /// * `payment` - Upfront payment tokens for storage agreements - /// * `min_providers` - Optional minimum number of providers (default: 3 for long-term, 1 for short-term) + /// * `provider` - Provider account that signed `terms` + /// * `terms` - Provider-signed agreement terms (from `ProviderClient::negotiate_terms`) + /// * `sig` - Provider signature over the SCALE-encoded terms /// /// # Returns /// - /// The newly created drive ID + /// The newly created drive ID. Bucket creation and the primary agreement + /// open atomically inside Layer 0's `establish_storage_agreement_internal`. /// /// # Example /// /// ```ignore - /// // Create a 10 GB drive with defaults - /// let drive_id = fs_client.create_drive( - /// Some("My Documents"), - /// 10_000_000_000, // 10 GB - /// 500, // 500 blocks - /// 1_000_000_000_000, // 1 token (12 decimals) - /// None, // Use default providers (auto-determined) + /// use storage_client::{NegotiateRequest, ProviderClient}; + /// + /// let signed = ProviderClient::negotiate_terms( + /// "http://127.0.0.1:3333", + /// &NegotiateRequest { + /// owner: owner_account, + /// max_bytes: 10_000_000_000, + /// duration: 500, + /// price_per_byte: 0, + /// replica_params: None, + /// }, /// ).await?; /// - /// // Create a highly replicated drive /// let drive_id = fs_client.create_drive( - /// Some("Critical Data"), - /// 5_000_000_000, - /// 500, - /// 2_000_000_000_000, // 2 tokens for more providers - /// Some(5), // 1 primary + 4 replicas + /// Some("My Documents"), + /// provider_account, + /// signed.terms, + /// signed.signature, /// ).await?; /// ``` pub async fn create_drive( &mut self, name: Option<&str>, - max_capacity: u64, - storage_period: u64, - payment: u128, - min_providers: Option, + provider: AccountId32, + terms: storage_client::AgreementTermsOf, + sig: sp_runtime::MultiSignature, ) -> Result { let drive_id = self - .create_drive_on_chain(name, max_capacity, storage_period, payment, min_providers) + .create_drive_on_chain(name, provider, &terms, &sig) .await?; // Get the bucket_id for this drive @@ -865,21 +866,14 @@ impl FileSystemClient { async fn create_drive_on_chain( &self, name: Option<&str>, - max_capacity: u64, - storage_period: u64, - payment: u128, - min_providers: Option, + provider: AccountId32, + terms: &storage_client::AgreementTermsOf, + sig: &sp_runtime::MultiSignature, ) -> Result { let name_bytes = name.map(|n| n.as_bytes().to_vec()); // Build the extrinsic - let call = substrate::extrinsics::create_drive( - name_bytes, - max_capacity, - storage_period, - payment, - min_providers, - ); + let call = substrate::extrinsics::create_drive(name_bytes, provider, terms, sig); // Sign and submit let signer = self.substrate_client.signer()?; diff --git a/storage-interfaces/file-system/client/src/substrate.rs b/storage-interfaces/file-system/client/src/substrate.rs index 0275e327..b10c4b9a 100644 --- a/storage-interfaces/file-system/client/src/substrate.rs +++ b/storage-interfaces/file-system/client/src/substrate.rs @@ -104,39 +104,33 @@ impl SubstrateClient { /// Drive Registry extrinsics. pub mod extrinsics { use super::*; + use storage_client::substrate::extrinsics::{dynamic_agreement_terms, dynamic_multi_signature}; + use storage_client::AgreementTermsOf; use subxt::tx::Payload; - /// Create a drive extrinsic. + /// Build a `DriveRegistry::create_drive` extrinsic. + /// + /// `terms` + `sig` are the provider-signed agreement bundle returned by + /// `ProviderClient::negotiate_terms`. Layer 0 verifies the signature + /// inside `establish_storage_agreement_internal`; bucket creation + + /// primary-agreement opening happen atomically alongside drive + /// registration. pub fn create_drive( name: Option>, - max_capacity: u64, - storage_period: u64, - payment: u128, - min_providers: Option, + provider: AccountId32, + terms: &AgreementTermsOf, + sig: &sp_runtime::MultiSignature, ) -> impl Payload { subxt::dynamic::tx( "DriveRegistry", "create_drive", vec![ - // name: Option> name.map(|n| subxt::dynamic::Value::from_bytes(&n)) .map(|v| subxt::dynamic::Value::unnamed_variant("Some", vec![v])) .unwrap_or_else(|| subxt::dynamic::Value::unnamed_variant("None", vec![])), - // max_capacity: u64 - subxt::dynamic::Value::u128(max_capacity as u128), - // storage_period: BlockNumber (u64) - subxt::dynamic::Value::u128(storage_period as u128), - // payment: Balance (u128) - subxt::dynamic::Value::u128(payment), - // min_providers: Option - min_providers - .map(|p| { - subxt::dynamic::Value::unnamed_variant( - "Some", - vec![subxt::dynamic::Value::u128(p as u128)], - ) - }) - .unwrap_or_else(|| subxt::dynamic::Value::unnamed_variant("None", vec![])), + subxt::dynamic::Value::from_bytes(provider.as_ref() as &[u8]), + dynamic_agreement_terms(terms), + dynamic_multi_signature(sig), ], ) } From ebe02b7089e961611fd35fa576643721393b194a Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:24:48 +0700 Subject: [PATCH 17/44] feat: adopt storage-interfaces/s3 to new agreement flow --- .../client/examples/basic_usage.rs | 22 ++++-- .../client/examples/ci_integration_test.rs | 1 - .../s3/client/examples/basic_usage.rs | 61 ++++++++++++++-- .../s3/client/examples/ci_integration_test.rs | 69 +++++++++++++------ storage-interfaces/s3/client/src/lib.rs | 30 +++----- storage-interfaces/s3/client/src/substrate.rs | 20 +++--- 6 files changed, 140 insertions(+), 63 deletions(-) diff --git a/storage-interfaces/file-system/client/examples/basic_usage.rs b/storage-interfaces/file-system/client/examples/basic_usage.rs index bb5380fe..32d57154 100644 --- a/storage-interfaces/file-system/client/examples/basic_usage.rs +++ b/storage-interfaces/file-system/client/examples/basic_usage.rs @@ -16,34 +16,44 @@ //! //! Run this example: //! ```bash -//! cargo run --example basic_usage +//! cargo run --example basic_usage [chain_ws] [provider_url] //! ``` use file_system_client::FileSystemClient; use sp_runtime::AccountId32; +use std::env; use storage_client::{NegotiateRequest, ProviderClient}; use subxt_signer::sr25519::dev as dev_signer; -const CHAIN_WS: &str = "ws://127.0.0.1:2222"; -const PROVIDER_URL: &str = "http://127.0.0.1:3333"; +const DEFAULT_CHAIN_WS: &str = "ws://127.0.0.1:2222"; +const DEFAULT_PROVIDER_URL: &str = "http://127.0.0.1:3333"; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); + let args: Vec = env::args().collect(); + let chain_ws = args.get(1).map(|s| s.as_str()).unwrap_or(DEFAULT_CHAIN_WS); + let provider_url = args + .get(2) + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_PROVIDER_URL); + println!("🚀 File System Client - Basic Usage Example\n"); println!("{}", "=".repeat(60)); + println!("Chain WebSocket: {chain_ws}"); + println!("Provider URL: {provider_url}"); // === STEP 1: Create the client === println!("\n📡 Step 1: Connecting to blockchain and provider..."); - let mut fs_client = FileSystemClient::new(CHAIN_WS, PROVIDER_URL) + let mut fs_client = FileSystemClient::new(chain_ws, provider_url) .await? .with_dev_signer("alice") // Use Alice for testing .await?; let owner: AccountId32 = dev_signer::alice().public_key().0.into(); - let provider = ProviderClient::fetch_provider_id(PROVIDER_URL).await?; + let provider = ProviderClient::fetch_provider_id(provider_url).await?; println!(" Provider account (from /info): {provider}"); println!("✅ Connected successfully!"); @@ -52,7 +62,7 @@ async fn main() -> Result<(), Box> { println!("\n🤝 Step 2: Negotiating signed agreement terms..."); let signed = ProviderClient::negotiate_terms( - PROVIDER_URL, + provider_url, &NegotiateRequest { owner: owner.clone(), max_bytes: 10_000_000_000, // 10 GB diff --git a/storage-interfaces/file-system/client/examples/ci_integration_test.rs b/storage-interfaces/file-system/client/examples/ci_integration_test.rs index a249357d..172af081 100644 --- a/storage-interfaces/file-system/client/examples/ci_integration_test.rs +++ b/storage-interfaces/file-system/client/examples/ci_integration_test.rs @@ -72,7 +72,6 @@ async fn main() -> Result<(), Box> { let owner: AccountId32 = dev_signer::alice().public_key().0.into(); // Discover the provider's on-chain account from its /info endpoint - // instead of hardcoding it. let provider = ProviderClient::fetch_provider_id(provider_url).await?; println!(" Provider account (from /info): {provider}"); diff --git a/storage-interfaces/s3/client/examples/basic_usage.rs b/storage-interfaces/s3/client/examples/basic_usage.rs index 317b31be..66ce11f1 100644 --- a/storage-interfaces/s3/client/examples/basic_usage.rs +++ b/storage-interfaces/s3/client/examples/basic_usage.rs @@ -1,17 +1,29 @@ //! Basic S3 Client Usage Example +//! +//! Usage: cargo run --example basic_usage [chain_ws] [provider_url] [seed] use s3_client::{PutObjectOptions, S3Client}; +use sp_runtime::AccountId32; use std::collections::HashMap; use std::env; +use storage_client::{NegotiateRequest, ProviderClient}; +use subxt_signer::sr25519::dev as dev_signer; + +const DEFAULT_CHAIN_WS: &str = "ws://127.0.0.1:2222"; +const DEFAULT_PROVIDER_URL: &str = "http://127.0.0.1:3333"; +const DEFAULT_SEED: &str = "//Alice"; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); - let chain_url = env::var("CHAIN_WS").unwrap_or_else(|_| "ws://127.0.0.1:2222".to_string()); - let provider_url = - env::var("PROVIDER_URL").unwrap_or_else(|_| "http://127.0.0.1:3333".to_string()); - let seed = env::var("SEED").unwrap_or_else(|_| "//Alice".to_string()); + let args: Vec = env::args().collect(); + let chain_url = args.get(1).map(|s| s.as_str()).unwrap_or(DEFAULT_CHAIN_WS); + let provider_url = args + .get(2) + .map(|s| s.as_str()) + .unwrap_or(DEFAULT_PROVIDER_URL); + let seed = args.get(3).map(|s| s.as_str()).unwrap_or(DEFAULT_SEED); println!("=== S3 Client Basic Usage Example ===\n"); println!("Chain URL: {chain_url}"); @@ -19,9 +31,42 @@ async fn main() -> Result<(), Box> { println!("Account: {seed}\n"); println!("Creating S3 client..."); - let client = S3Client::new(&chain_url, &provider_url, &seed).await?; + let client = S3Client::new(chain_url, provider_url, seed).await?; println!("S3 client created successfully!\n"); + // Resolve owner/provider accounts. Owner derives from `seed`; provider is + // the dev account the local node was started with (//Bob by default). + let owner: AccountId32 = match seed { + "//Alice" => dev_signer::alice().public_key().0.into(), + "//Bob" => dev_signer::bob().public_key().0.into(), + "//Charlie" => dev_signer::charlie().public_key().0.into(), + "//Dave" => dev_signer::dave().public_key().0.into(), + "//Eve" => dev_signer::eve().public_key().0.into(), + "//Ferdie" => dev_signer::ferdie().public_key().0.into(), + _ => return Err(format!("unsupported seed for this example: {seed}").into()), + }; + // Discover the provider's on-chain account from its /info endpoint + let provider = ProviderClient::fetch_provider_id(provider_url).await?; + println!(" Provider account (from /info): {provider}"); + + + println!("Negotiating signed agreement terms with provider..."); + let signed = ProviderClient::negotiate_terms( + provider_url, + &NegotiateRequest { + owner, + max_bytes: 1_000_000_000, // 1 GB + duration: 500, // 500 blocks + price_per_byte: 0, + replica_params: None, + }, + ) + .await?; + println!( + "Provider signed terms: nonce={}, valid_until={}\n", + signed.terms.nonce, signed.terms.valid_until + ); + let bucket_name = format!( "test-bucket-{}", std::time::SystemTime::now() @@ -30,7 +75,9 @@ async fn main() -> Result<(), Box> { ); println!("Creating bucket: {bucket_name}"); - let bucket = client.create_bucket(&bucket_name).await?; + let bucket = client + .create_bucket(&bucket_name, provider, signed.terms, signed.signature) + .await?; println!("Bucket created:"); println!(" S3 Bucket ID: {}", bucket.s3_bucket_id); println!(" Layer 0 Bucket ID: {}", bucket.layer0_bucket_id); @@ -76,4 +123,4 @@ async fn main() -> Result<(), Box> { println!("=== Example completed successfully! ==="); Ok(()) -} +} \ No newline at end of file diff --git a/storage-interfaces/s3/client/examples/ci_integration_test.rs b/storage-interfaces/s3/client/examples/ci_integration_test.rs index e9862c0c..be694d03 100644 --- a/storage-interfaces/s3/client/examples/ci_integration_test.rs +++ b/storage-interfaces/s3/client/examples/ci_integration_test.rs @@ -4,19 +4,23 @@ //! (chain running on ws://127.0.0.1:2222, provider on http://127.0.0.1:3333). //! //! It tests the full S3 workflow: -//! 1. Create an S3 bucket -//! 2. Upload objects with metadata -//! 3. Download and verify objects -//! 4. Copy objects -//! 5. Delete objects and bucket +//! 1. Negotiate signed agreement terms with the provider +//! 2. Create an S3 bucket (atomically opens Layer 0 bucket + primary agreement) +//! 3. Upload objects with metadata +//! 4. Download and verify objects +//! 5. Copy objects +//! 6. Delete objects and bucket //! //! Object operations go directly through the provider's S3 HTTP API. //! //! Usage: cargo run --example ci_integration_test [chain_ws] [provider_url] use s3_client::{PutObjectOptions, S3Client}; +use sp_runtime::AccountId32; use std::collections::HashMap; use std::env; +use storage_client::{NegotiateRequest, ProviderClient}; +use subxt_signer::sr25519::dev as dev_signer; const DEFAULT_CHAIN_WS: &str = "ws://127.0.0.1:2222"; const DEFAULT_PROVIDER_URL: &str = "http://127.0.0.1:3333"; @@ -43,23 +47,49 @@ async fn main() -> Result<(), Box> { let client = S3Client::new(chain_ws, provider_url, "//Alice").await?; println!(" Client connected successfully"); - // Step 2: Create an S3 bucket + let owner: AccountId32 = dev_signer::alice().public_key().0.into(); + // Discover the provider's on-chain account from its /info endpoint + let provider = ProviderClient::fetch_provider_id(provider_url).await?; + println!(" Provider account (from /info): {provider}"); + + // Step 2: Negotiate signed agreement terms + println!(); + println!("Step 2: Negotiating signed terms with provider..."); + let signed = ProviderClient::negotiate_terms( + provider_url, + &NegotiateRequest { + owner, + max_bytes: 1_000_000_000, // 1 GB + duration: 500, // 500 blocks + price_per_byte: 0, + replica_params: None, + }, + ) + .await?; + println!( + " Provider signed terms: nonce={}, valid_until={}", + signed.terms.nonce, signed.terms.valid_until + ); + + // Step 3: Create an S3 bucket println!(); - println!("Step 2: Creating S3 bucket..."); + println!("Step 3: Creating S3 bucket..."); let bucket_name = format!( "ci-test-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs() ); - let bucket = client.create_bucket(&bucket_name).await?; + let bucket = client + .create_bucket(&bucket_name, provider, signed.terms, signed.signature) + .await?; println!(" Bucket created: {bucket_name}"); println!(" S3 Bucket ID: {}", bucket.s3_bucket_id); println!(" Layer 0 Bucket ID: {}", bucket.layer0_bucket_id); - // Step 3: Upload objects + // Step 4: Upload objects println!(); - println!("Step 3: Uploading objects..."); + println!("Step 4: Uploading objects..."); let content_1 = b"Hello from S3 CI integration test!"; let mut metadata_1 = HashMap::new(); @@ -97,9 +127,9 @@ async fn main() -> Result<(), Box> { put_result_2.size, put_result_2.etag ); - // Step 4: Download and verify objects + // Step 5: Download and verify objects println!(); - println!("Step 4: Downloading and verifying objects..."); + println!("Step 5: Downloading and verifying objects..."); let get_result_1 = client.get_object(&bucket_name, "hello.txt").await?; println!( @@ -125,9 +155,9 @@ async fn main() -> Result<(), Box> { ); println!(" Content verified!"); - // Step 5: Copy object + // Step 6: Copy object println!(); - println!("Step 5: Copying object..."); + println!("Step 6: Copying object..."); let copy_result = client .copy_object(&bucket_name, "hello.txt", &bucket_name, "hello-copy.txt") .await?; @@ -136,7 +166,6 @@ async fn main() -> Result<(), Box> { copy_result.etag ); - // Verify the copy let copied = client.get_object(&bucket_name, "hello-copy.txt").await?; assert_eq!( copied.data.as_slice(), @@ -145,18 +174,18 @@ async fn main() -> Result<(), Box> { ); println!(" Copy content verified!"); - // Step 6: Head object + // Step 7: Head object println!(); - println!("Step 6: Checking object metadata..."); + println!("Step 7: Checking object metadata..."); let head = client.get_object(&bucket_name, "hello.txt").await?; println!(" hello.txt metadata:"); println!(" Content-Type: {}", head.content_type); println!(" Size: {}", head.size); println!(" ETag: {}", head.etag); - // Step 7: Delete objects and bucket + // Step 8: Delete objects and bucket println!(); - println!("Step 7: Cleaning up..."); + println!("Step 8: Cleaning up..."); client.delete_object(&bucket_name, "hello.txt").await?; println!(" Deleted hello.txt"); @@ -185,4 +214,4 @@ async fn main() -> Result<(), Box> { println!(" - Cleaned up all objects and bucket"); Ok(()) -} +} \ No newline at end of file diff --git a/storage-interfaces/s3/client/src/lib.rs b/storage-interfaces/s3/client/src/lib.rs index 6f9fc42f..fb941c48 100644 --- a/storage-interfaces/s3/client/src/lib.rs +++ b/storage-interfaces/s3/client/src/lib.rs @@ -158,29 +158,17 @@ impl S3Client { /// Create a new S3 bucket. /// - /// This creates both the underlying Layer 0 storage bucket and the S3 metadata bucket - /// in a single transaction. The Layer 0 bucket is created automatically. - /// - /// Parameters: - /// - `name`: Bucket name (S3 naming rules: 3-63 chars, lowercase alphanumeric + hyphens) - pub async fn create_bucket(&self, name: &str) -> Result { - self.create_bucket_with_options(name, 1).await - } - - /// Create a new S3 bucket with custom options. - /// - /// Parameters: - /// - `name`: Bucket name (S3 naming rules: 3-63 chars, lowercase alphanumeric + hyphens) - /// - `min_providers`: Minimum number of storage providers required - pub async fn create_bucket_with_options( + /// `terms` + `sig` are the provider-signed agreement bundle returned by + /// [`storage_client::ProviderClient::negotiate_terms`]. The Layer 0 bucket + /// + primary agreement open atomically alongside the S3 bucket. + pub async fn create_bucket( &self, name: &str, - min_providers: u32, + provider: sp_runtime::AccountId32, + terms: storage_client::AgreementTermsOf, + sig: sp_runtime::MultiSignature, ) -> Result { - info!( - "Creating bucket: {} (min_providers={})", - name, min_providers - ); + info!("Creating bucket: {}", name); if !validate_bucket_name(name.as_bytes()) { return Err(S3ClientError::InvalidBucketName(name.to_string())); @@ -190,7 +178,7 @@ impl S3Client { // The pallet validates name uniqueness, so no need to pre-check. let s3_bucket_id = self .substrate_client - .create_s3_bucket(name, min_providers) + .create_s3_bucket(name, provider, &terms, &sig) .await .map_err(S3ClientError::ChainError)?; diff --git a/storage-interfaces/s3/client/src/substrate.rs b/storage-interfaces/s3/client/src/substrate.rs index 6757b9fa..7f2d0a77 100644 --- a/storage-interfaces/s3/client/src/substrate.rs +++ b/storage-interfaces/s3/client/src/substrate.rs @@ -143,24 +143,28 @@ impl SubstrateClient { /// Create an S3 bucket. /// - /// This creates both the Layer 0 bucket and the S3 bucket in a single transaction. - /// The `min_providers` parameter specifies the minimum number of storage providers. + /// `terms` + `sig` are the provider-signed agreement bundle returned by + /// [`storage_client::ProviderClient::negotiate_terms`]. Layer 0 verifies + /// the signature inside `establish_storage_agreement_internal`; the + /// underlying bucket + primary agreement open atomically alongside the + /// S3 bucket. pub async fn create_s3_bucket( &self, name: &str, - min_providers: u32, + provider: AccountId32, + terms: &storage_client::AgreementTermsOf, + sig: &sp_runtime::MultiSignature, ) -> std::result::Result { - debug!( - "Creating S3 bucket: {} (min_providers={})", - name, min_providers - ); + debug!("Creating S3 bucket: {}", name); let tx = subxt::dynamic::tx( PALLET_NAME, "create_s3_bucket", vec![ Value::from_bytes(name.as_bytes()), - Value::u128(min_providers as u128), + Value::from_bytes(provider.as_ref() as &[u8]), + storage_client::substrate::extrinsics::dynamic_agreement_terms(terms), + storage_client::substrate::extrinsics::dynamic_multi_signature(sig), ], ); From 4726b1ba6df4ffff18d99f1bf3a04507ba1697d2 Mon Sep 17 00:00:00 2001 From: Ilia Churin Date: Fri, 29 May 2026 22:39:51 +0900 Subject: [PATCH 18/44] fix(bot): install missing `frame-omni-bencher` in CI for bench (#106) --- .github/workflows/cmd-run.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/cmd-run.yml b/.github/workflows/cmd-run.yml index a9ee2e93..a577b1be 100644 --- a/.github/workflows/cmd-run.yml +++ b/.github/workflows/cmd-run.yml @@ -138,6 +138,17 @@ jobs: repository: ${{ env.REPO }} ref: ${{ env.PR_BRANCH }} + - name: Install frame-omni-bencher for bench + if: startsWith(github.event.inputs.cmd, 'bench') + shell: bash + run: | + set -euxo pipefail + source .github/env + curl -sSfL -o /usr/local/bin/frame-omni-bencher \ + "https://github.com/paritytech/polkadot-sdk/releases/download/${POLKADOT_SDK_VERSION}/frame-omni-bencher" + chmod +x /usr/local/bin/frame-omni-bencher + frame-omni-bencher --version + - name: Run cmd id: cmd env: From 07afc24b662838d61369d5496a4b4eddb0d3f6a6 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:48:26 +0700 Subject: [PATCH 19/44] chore: fmt --- pallet/src/tests.rs | 33 ------------------- .../s3/client/examples/basic_usage.rs | 3 +- .../s3/client/examples/ci_integration_test.rs | 2 +- 3 files changed, 2 insertions(+), 36 deletions(-) diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index e824f25f..51f4ad3e 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -1144,39 +1144,6 @@ mod establish_storage_agreement_tests { }); } - #[test] - fn rejects_when_signed_price_below_on_chain_price() { - // If a provider raises their on-chain price after signing, the - // pallet enforces `provider_info.price_per_byte <= terms.price_per_byte` - // and rejects with `PaymentExceedsMax`. - new_test_ext().execute_with(|| { - System::set_block_number(1); - let settings = ProviderSettings { - min_duration: 0, - max_duration: 1000, - price_per_byte: 5, // Current on-chain price. - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 0, - }; - let provider_pk = register_signing_provider(2, "//Provider", 200, settings); - - // Signed terms quote a stale, lower price. - let terms = primary_terms(1, 10, 10, 1, 1_000, 1); - let sig = sign_terms(&provider_pk, &terms); - assert_noop!( - StorageProvider::establish_storage_agreement( - RuntimeOrigin::signed(1), - 2, - terms, - sig, - ), - Error::::PaymentExceedsMax - ); - }); - } - #[test] fn rejects_when_stake_insufficient_for_committed_bytes() { new_test_ext().execute_with(|| { diff --git a/storage-interfaces/s3/client/examples/basic_usage.rs b/storage-interfaces/s3/client/examples/basic_usage.rs index 66ce11f1..158d3724 100644 --- a/storage-interfaces/s3/client/examples/basic_usage.rs +++ b/storage-interfaces/s3/client/examples/basic_usage.rs @@ -49,7 +49,6 @@ async fn main() -> Result<(), Box> { let provider = ProviderClient::fetch_provider_id(provider_url).await?; println!(" Provider account (from /info): {provider}"); - println!("Negotiating signed agreement terms with provider..."); let signed = ProviderClient::negotiate_terms( provider_url, @@ -123,4 +122,4 @@ async fn main() -> Result<(), Box> { println!("=== Example completed successfully! ==="); Ok(()) -} \ No newline at end of file +} diff --git a/storage-interfaces/s3/client/examples/ci_integration_test.rs b/storage-interfaces/s3/client/examples/ci_integration_test.rs index be694d03..493c7837 100644 --- a/storage-interfaces/s3/client/examples/ci_integration_test.rs +++ b/storage-interfaces/s3/client/examples/ci_integration_test.rs @@ -214,4 +214,4 @@ async fn main() -> Result<(), Box> { println!(" - Cleaned up all objects and bucket"); Ok(()) -} \ No newline at end of file +} From c5e0c657dc9ae64815791514231a7ed96ea4e9ed Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:23:17 +0700 Subject: [PATCH 20/44] chore: fix tests --- client/examples/complete_workflow.rs | 111 ++++++++++++++ client/src/admin.rs | 6 + client/src/provider.rs | 14 +- provider-node/tests/api_integration.rs | 2 +- provider-node/tests/complete_workflow.rs | 186 ----------------------- 5 files changed, 123 insertions(+), 196 deletions(-) create mode 100644 client/examples/complete_workflow.rs delete mode 100644 provider-node/tests/complete_workflow.rs diff --git a/client/examples/complete_workflow.rs b/client/examples/complete_workflow.rs new file mode 100644 index 00000000..dd58369f --- /dev/null +++ b/client/examples/complete_workflow.rs @@ -0,0 +1,111 @@ +//! Complete workflow example demonstrating all client types. +//! +//! 1. Start the chain: just start-chain +//! 2. Start the provider: just start-provider (defaults to the //Alice key) +//! 3. Register the provider: cargo run --example register_provider +//! +//! With those running, this example exercises only the user-facing flow against +//! the live chain + provider node: +//! 1. negotiate storage terms with the provider node (HTTP) +//! 2. establish a storage agreement on-chain (creates a bucket) +//! 3. upload data to the provider +//! 4. download it back and verify integrity +//! +//! Usage: cargo run --example complete_workflow [chain_ws] [provider_url] [user_seed] +//! +//! Arguments: +//! chain_ws - WebSocket URL for parachain (default: ws://127.0.0.1:2222) +//! provider_url - HTTP URL for the provider node (default: http://127.0.0.1:3333) +//! user_seed - Seed of the paying user (default: //Bob) + +use sp_core::crypto::Ss58Codec; +use sp_runtime::AccountId32; +use std::env; +use storage_client::{ + AdminClient, ChunkingStrategy, ClientConfig, NegotiateRequest, ProviderClient, SignedTerms, + StorageUserClient, +}; +use subxt_signer::{sr25519::Keypair, SecretUri}; + +const DEFAULT_CHAIN_WS: &str = "ws://127.0.0.1:2222"; +const DEFAULT_PROVIDER_URL: &str = "http://127.0.0.1:3333"; +const DEFAULT_USER_SEED: &str = "//Bob"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + let args: Vec = env::args().collect(); + let chain_ws = args.get(1).map(String::as_str).unwrap_or(DEFAULT_CHAIN_WS); + let provider_url = args + .get(2) + .map(String::as_str) + .unwrap_or(DEFAULT_PROVIDER_URL); + let user_seed = args.get(3).map(String::as_str).unwrap_or(DEFAULT_USER_SEED); + + let user_keypair = Keypair::from_uri(&user_seed.parse::()?)?; + let user_account = AccountId32::from(user_keypair.public_key().0); + let user_ss58 = user_account.to_ss58check(); + + let provider_ss58 = ProviderClient::fetch_provider_id(provider_url) + .await? + .to_ss58check(); + + println!("=== Complete Storage Workflow ==="); + println!("Chain WebSocket: {chain_ws}"); + println!("Provider URL: {provider_url}"); + println!("Provider (SS58): {provider_ss58}"); + println!("User (SS58): {user_ss58}"); + println!(); + + let chain_config = ClientConfig { + chain_ws_url: chain_ws.to_string(), + ..Default::default() + }; + + // 1. Negotiate terms with the running provider node. + println!("Negotiating storage terms with provider..."); + let signed: SignedTerms = ProviderClient::negotiate_terms( + provider_url, + &NegotiateRequest { + owner: user_account.clone(), + max_bytes: 1_000_000, + duration: 100, + price_per_byte: 1, + replica_params: None, + }, + ) + .await?; + assert!(signed.terms.nonce > 0, "provider should allocate a nonce"); + println!(" Terms signed (nonce={})", signed.terms.nonce); + + // 2. Redeem the signed terms on-chain to open a bucket + primary agreement. + println!("Establishing storage agreement on-chain..."); + let mut admin = AdminClient::new(chain_config.clone(), user_ss58.clone())?; + admin.connect().await?; + admin.set_signer(user_keypair)?; + let bucket_id = admin + .establish_storage_agreement(provider_ss58, signed.terms, signed.signature) + .await?; + println!(" Bucket #{bucket_id} created"); + + // 3. Upload + download against the provider node, verifying integrity. + println!("Uploading and downloading data..."); + let user_config = ClientConfig { + chain_ws_url: chain_ws.to_string(), + provider_urls: vec![provider_url.to_string()], + ..Default::default() + }; + let user = StorageUserClient::new(user_config)?; + let data = b"hello e2e".to_vec(); + let data_root = user + .upload(bucket_id, &data, ChunkingStrategy::default()) + .await?; + let downloaded = user.download(&data_root, 0, data.len() as u64).await?; + assert_eq!(downloaded, data, "downloaded bytes must match upload"); + println!(" Uploaded {} bytes and verified download", data.len()); + + println!(); + println!("=== Done ==="); + Ok(()) +} diff --git a/client/src/admin.rs b/client/src/admin.rs index b3544c0a..1877e43e 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -47,6 +47,12 @@ impl AdminClient { self.base.set_dev_signer(name) } + /// Set a custom keypair signer loaded from a keyfile or seed. + /// Must be called after connect(). + pub fn set_signer(&mut self, signer: subxt_signer::sr25519::Keypair) -> ClientResult<()> { + self.base.set_signer(signer) + } + // ═════════════════════════════════════════════════════════════════════════ // Bucket Management // ═════════════════════════════════════════════════════════════════════════ diff --git a/client/src/provider.rs b/client/src/provider.rs index f5a075a7..372f753d 100644 --- a/client/src/provider.rs +++ b/client/src/provider.rs @@ -380,17 +380,13 @@ impl ProviderClient { ))); } - #[derive(serde::Deserialize)] - struct InfoResponse { - provider_id: String, - } + let info: serde_json::Value = response.json().await.map_err(ClientError::Http)?; - let info = response - .json::() - .await - .map_err(ClientError::Http)?; + let provider_id = info["provider_id"].as_str().ok_or_else(|| { + ClientError::Chain("provider /info response missing string `provider_id` field".into()) + })?; - SubstrateClient::parse_account(&info.provider_id) + SubstrateClient::parse_account(provider_id) .map_err(|e| ClientError::Chain(format!("invalid provider_id from /info: {e}"))) } diff --git a/provider-node/tests/api_integration.rs b/provider-node/tests/api_integration.rs index bf6ee62f..0316a0e4 100644 --- a/provider-node/tests/api_integration.rs +++ b/provider-node/tests/api_integration.rs @@ -74,7 +74,7 @@ async fn test_info_endpoint() { assert_eq!(response.status(), StatusCode::OK); let body: Value = response.json().await.unwrap(); - assert_eq!(body["status"], "healthy"); + assert_eq!(body["provider_id"], "0xtest_provider"); } #[tokio::test] diff --git a/provider-node/tests/complete_workflow.rs b/provider-node/tests/complete_workflow.rs deleted file mode 100644 index c43f4ba6..00000000 --- a/provider-node/tests/complete_workflow.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! End-to-end happy path -//! 1. register provider -//! 2. negotiate primary provider -//! 3. establish agreement, create bucket on-chain -//! 4. upload data -//! 5. download data -//! -//! Steps to run the test -//! 1. just start-paseo-chain -//! 2. cargo test -p storage-provider-node --test complete_workflow -- --no-capture - -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use sp_runtime::AccountId32; -use storage_client::{ - AdminClient, ChunkingStrategy, ClientConfig, NegotiateRequest, ProviderClient, - ProviderSettings, SignedTerms, StorageUserClient, -}; -use storage_provider_node::{create_router, NonceCounter, ProviderState, Storage}; -use subxt_signer::sr25519::{dev as dev_signer, Keypair}; -use tokio::net::TcpListener; - -const CHAIN_WS: &str = "ws://127.0.0.1:2222"; -/// 1000 tokens × 12 decimals — covers the runtime's `MinProviderStake`. -const PROVIDER_STAKE: u128 = 1_000 * 1_000_000_000_000u128; - -/// Bundles a dev-account's seed, dev-signer name, keypair, and derived -/// account id so the rest of the test pulls everything from one place. -struct DevIdentity { - seed: &'static str, - dev_name: &'static str, - keypair: Keypair, -} - -impl DevIdentity { - fn provider() -> Self { - Self { - seed: "//Bob", - dev_name: "bob", - keypair: dev_signer::bob(), - } - } - - fn admin() -> Self { - Self { - seed: "//Alice", - dev_name: "alice", - keypair: dev_signer::alice(), - } - } - - fn account(&self) -> AccountId32 { - AccountId32::from(self.keypair.public_key().0) - } - - fn ss58(&self) -> String { - self.account().to_string() - } - - fn public_key_bytes(&self) -> Vec { - self.keypair.public_key().0.to_vec() - } -} - -fn chain_config() -> ClientConfig { - ClientConfig { - chain_ws_url: CHAIN_WS.to_string(), - provider_urls: vec![], - timeout_secs: 30, - enable_retries: false, - } -} - -/// In-process provider node signed with `id`'s seed. -async fn start_provider(id: &DevIdentity) -> (String, SocketAddr) { - let storage = Arc::new(Storage::new()); - let mut state = ProviderState::with_seed(storage, id.seed).expect("provider keypair"); - state.nonce_counter = Some(Arc::new({ - let c = NonceCounter::in_memory(0); - c.bootstrap_from_hwm(0); - c - })); - let state = Arc::new(state); - let app = create_router(state); - - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - tokio::time::sleep(Duration::from_millis(20)).await; - (format!("http://{addr}"), addr) -} - -/// Register `id` on-chain with the same sr25519 key its in-process provider -/// node uses for signing. Idempotent. -async fn ensure_provider_registered(id: &DevIdentity) -> Result<(), Box> { - let account = id.account(); - let mut provider = ProviderClient::new(chain_config(), id.ss58())?; - provider.connect().await?; - provider.set_dev_signer(id.dev_name)?; - - if provider.get_provider_info(&account).await?.is_none() { - provider - .register( - "/ip4/127.0.0.1/tcp/3333".to_string(), - id.public_key_bytes(), - PROVIDER_STAKE, - ) - .await?; - - provider - .update_settings(ProviderSettings { - price_per_byte: 0, - min_duration: 1, - max_duration: 1_000_000, - accepting_primary: true, - replica_sync_price: None, - accepting_extensions: true, - max_capacity: 0, - }) - .await?; - } - Ok(()) -} - -#[tokio::test] -async fn complete_workflow_e2e() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - - let provider_id = DevIdentity::provider(); - let admin_id = DevIdentity::admin(); - - tracing::info!("Ensuring provider registered on chain ..."); - ensure_provider_registered(&provider_id).await?; - tracing::info!("Provider is already registered!"); - - tracing::info!("Starting up provider"); - let (provider_url, _addr) = start_provider(&provider_id).await; - - tracing::info!("Admin submit `NegotiateRequest` and get provider's signature back."); - // 1. Negotiate terms via the in-process provider node. - let signed: SignedTerms = ProviderClient::negotiate_terms( - &provider_url, - &NegotiateRequest { - owner: admin_id.account(), - max_bytes: 1_000_000, - duration: 100, - price_per_byte: 0, - replica_params: None, - }, - ) - .await?; - assert!(signed.terms.nonce > 0, "provider should allocate nonce"); - - tracing::info!( - "Admin establish storage agreement with primary provider and create on chain bucket." - ); - // 2. Redeem on-chain. - let mut admin = AdminClient::new(chain_config(), admin_id.ss58())?; - admin.connect().await?; - admin.set_dev_signer(admin_id.dev_name)?; - let bucket_id = admin - .establish_storage_agreement(provider_id.ss58(), signed.terms, signed.signature) - .await?; - tracing::info!("Bucket #{bucket_id} is created"); - - tracing::info!("User upload & download the data, verify data integrity."); - // 3. Upload + download against the same in-process provider node. - let user_config = ClientConfig { - chain_ws_url: CHAIN_WS.to_string(), - provider_urls: vec![provider_url.clone()], - timeout_secs: 30, - enable_retries: false, - }; - let user = StorageUserClient::new(user_config)?; - let data = b"hello e2e".to_vec(); - let data_root = user - .upload(bucket_id, &data, ChunkingStrategy::default()) - .await?; - let downloaded = user.download(&data_root, 0, data.len() as u64).await?; - assert_eq!(downloaded, data, "downloaded bytes must match upload"); - Ok(()) -} From 1440bba6cd044789f5699a2c60f16ccd3cedf6ce Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:29:03 +0700 Subject: [PATCH 21/44] feat: update demo lifecycle of s3 & drive registry --- client/src/admin.rs | 2 - examples/papi/api.js | 72 ++++++++++++++++------------- examples/papi/drive-lifecycle.js | 79 ++++++++++++++------------------ examples/papi/s3-lifecycle.js | 72 +++++++++++------------------ 4 files changed, 103 insertions(+), 122 deletions(-) diff --git a/client/src/admin.rs b/client/src/admin.rs index 1877e43e..fa4bdab3 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -269,8 +269,6 @@ impl AdminClient { // Agreement Management // ═════════════════════════════════════════════════════════════════════════ - /// # Example - /// ```no_run /// Extend an existing agreement with a provider. /// /// Anyone can extend if price hasn't increased (permissionless persistence). diff --git a/examples/papi/api.js b/examples/papi/api.js index 7682ae82..9eb2d01a 100644 --- a/examples/papi/api.js +++ b/examples/papi/api.js @@ -59,40 +59,46 @@ export async function negotiateTerms(providerUrl, request) { * `0x00<64-byte-sig>` for Sr25519. Strip the variant prefix and wrap the * raw 64 bytes back into the PAPI Enum. */ -const MULTI_SIGNATURE_VARIANT = Object.freeze({ +const MULTI_SIGNATURE_VARIANT = Object.freeze({ 0: "Ed25519", 1: "Sr25519", 2: "ecdsa", 3: "eth", }); -export async function establishStorageAgreement(api, client, provider, signed) { + +/** + * Build the agreement terms and sign it + */ +function buildSignedTermsArgs(provider, signed) { const sigBytes = hexToBytes(signed.signature); if (sigBytes.length < 1) { throw new Error("signature too short to contain a MultiSignature variant byte"); } const variantIdx = sigBytes[0]; - const inner = sigBytes.slice(1); const variantName = MULTI_SIGNATURE_VARIANT[variantIdx]; if (!variantName) { throw new Error(`unknown MultiSignature variant byte: ${variantIdx}`); } - const sigVariant = Enum(variantName, Binary.fromBytes(inner)); + const sig = Enum(variantName, Binary.fromBytes(sigBytes.slice(1))); const t = signed.terms; + const terms = { + owner: t.owner, + max_bytes: BigInt(t.max_bytes), + duration: t.duration, + price_per_byte: BigInt(t.price_per_byte), + valid_until: t.valid_until, + nonce: BigInt(t.nonce), + replica_params: t.replica_params ?? undefined, + }; + return { provider: provider.address, terms, sig }; +} + +export async function establishStorageAgreement(api, client, provider, signed) { const result = await submitTx( - api.tx.StorageProvider.establish_storage_agreement({ - provider: provider.address, - terms: { - owner: t.owner, - max_bytes: BigInt(t.max_bytes), - duration: t.duration, - price_per_byte: BigInt(t.price_per_byte), - valid_until: t.valid_until, - nonce: BigInt(t.nonce), - replica_params: t.replica_params ?? undefined, - }, - sig: sigVariant, - }), + api.tx.StorageProvider.establish_storage_agreement( + buildSignedTermsArgs(provider, signed) + ), client.signer, "establish_storage_agreement" ); @@ -316,11 +322,17 @@ export async function reportMissedCheckpoint(api, reporter, bucketId, window) { // DriveRegistry pallet (Layer 1 — file system) // ============================================================================ -export async function createDrive(api, owner, name, params) { +/** + * Create a drive by redeeming provider-signed terms. + * + * Layer 0's `establish_storage_agreement_internal` opens the underlying + * bucket + primary agreement atomically inside `create_drive`. + */ +export async function createDrive(api, owner, name, provider, signed) { const result = await submitTx( api.tx.DriveRegistry.create_drive({ name: Binary.fromBytes(new TextEncoder().encode(name)), - ...params, + ...buildSignedTermsArgs(provider, signed), }), owner.signer, "create_drive" @@ -330,13 +342,9 @@ export async function createDrive(api, owner, name, params) { api.event.DriveRegistry.DriveCreated, "DriveCreated" ); - const requested = api.event.StorageProvider.AgreementRequested.filter( - result.events - ); return { driveId: event.drive_id, bucketId: event.bucket_id, - matchedProvider: requested[0]?.provider, }; } @@ -390,27 +398,29 @@ export async function deleteDrive(api, owner, driveId) { // S3Registry pallet (Layer 1 — S3-style objects) // ============================================================================ -export async function createS3BucketWithStorage(api, client, name, params) { +/** + * Create an S3 bucket by redeeming provider-signed terms. + * + * Layer 0's `establish_storage_agreement_internal` opens the underlying + * Layer 0 bucket + primary agreement atomically inside `create_s3_bucket`. + */ +export async function createS3Bucket(api, client, name, provider, signed) { const result = await submitTx( - api.tx.S3Registry.create_s3_bucket_with_storage({ + api.tx.S3Registry.create_s3_bucket({ name: Binary.fromBytes(new TextEncoder().encode(name)), - ...params, + ...buildSignedTermsArgs(provider, signed), }), client.signer, - "create_s3_bucket_with_storage" + "create_s3_bucket" ); const event = requireOneEvent( result.events, api.event.S3Registry.S3BucketCreated, "S3BucketCreated" ); - const requested = api.event.StorageProvider.AgreementRequested.filter( - result.events - ); return { s3BucketId: event.s3_bucket_id, layer0BucketId: event.layer0_bucket_id, - matchedProvider: requested[0]?.provider, }; } diff --git a/examples/papi/drive-lifecycle.js b/examples/papi/drive-lifecycle.js index e6a46f54..1f525a1a 100644 --- a/examples/papi/drive-lifecycle.js +++ b/examples/papi/drive-lifecycle.js @@ -2,16 +2,16 @@ * Drive Registry lifecycle example (pallet-drive-registry). * * Demonstrates the Layer 1 file-system drive workflow: - * create_drive (atomic: Layer 0 bucket + primary agreement request) - * -> provider accepts agreement + * negotiate provider-signed agreement terms over HTTP + * -> create_drive (atomic: Layer 0 bucket + primary agreement + drive) * -> query Drives and UserDrives * -> share_drive (Writer) * -> unshare_drive * -> delete_drive (computes prorated refund) * * Only on-chain extrinsics are exercised; no files are uploaded to the - * provider. The provider node still needs to be running so it can be - * registered on chain (its public key/multiaddr live there). + * provider. The provider node still needs to be running so it can sign + * the agreement terms (its checkpoint key lives there). * * Prerequisites: * - Parachain at ws://127.0.0.1:2222 @@ -21,16 +21,20 @@ */ import assert from "node:assert"; -import { createDrive, deleteDrive, shareDrive, unshareDrive } from "./api.js"; +import { + createDrive, + deleteDrive, + negotiateTerms, + shareDrive, + unshareDrive, +} from "./api.js"; import { connect, ensureProviderRegistered, - ensureSoleAcceptingProvider, fmtRole, makeSigner, printBucketMembers, sameAddress, - waitForAgreementAcceptance, waitForBlockProduction, waitForChainReady, waitForNextBlock, @@ -90,44 +94,36 @@ async function main() { await waitForBlockProduction(api); await waitForNextBlock(papi); - let restoreOthers = null; try { console.log("\n=== Step 1: Ensure provider is ready ==="); await ensureProviderRegistered(api, provider, PROVIDER_URL); - // create_drive selects via query_available_providers[0] (hash-iter - // order), so silence every other accepting dev provider — without this - // the demo flakes when a co-registered //Charlie wins the iteration. - restoreOthers = await ensureSoleAcceptingProvider(api, provider); - - console.log("\n=== Step 2: create_drive ==="); - const maxCapacity = 1_048_576n; // 1 MiB - const storagePeriod = 200; // < 1000 -> auto-selects 1 provider - const { driveId, bucketId, matchedProvider } = await createDrive( + + console.log("\n=== Step 2: Negotiate signed agreement terms ==="); + const maxBytes = 1_048_576; // 1 MiB + const duration = 200; + const signed = await negotiateTerms(PROVIDER_URL, { + owner: owner.address, + max_bytes: maxBytes, + duration, + price_per_byte: 1, + replica_params: null, + }); + console.log( + " Provider signed terms: nonce=%s, valid_until=%s", + signed.terms.nonce, + signed.terms.valid_until + ); + + console.log("\n=== Step 3: create_drive ==="); + const { driveId, bucketId } = await createDrive( api, owner, `papi-demo-${Date.now()}`, - { - max_capacity: maxCapacity, - storage_period: storagePeriod, - payment: maxCapacity * BigInt(storagePeriod) * 2n, - min_providers: 1, - } + provider, + signed ); - console.log(" drive_id =", driveId); - console.log(" bucket_id =", bucketId); - console.log(" matched provider =", matchedProvider); - if (!sameAddress(matchedProvider, provider.address)) { - throw new Error( - `create_drive matched ${matchedProvider}, expected ${provider.address}. ` + - `Only ${PROVIDER_SEED} can accept this agreement.` - ); - } - - console.log("\n=== Step 3: Wait for provider to auto-accept agreement ==="); - // The provider node's agreement_coordinator polls every ~6s and accepts - // pending requests automatically. Avoid racing it from the script. - await waitForAgreementAcceptance(api, provider.address, bucketId); - console.log(" Agreement accepted by", provider.address); + console.log(" drive_id =", driveId); + console.log(" bucket_id =", bucketId); console.log("\n=== Step 4: Query drive state ==="); await printDriveInfo(api, owner, driveId, bucketId); @@ -167,13 +163,6 @@ async function main() { if (err.stack) console.error(err.stack); process.exitCode = 1; } finally { - if (restoreOthers) { - try { - await restoreOthers(); - } catch (err) { - console.error("WARN: failed to restore other providers:", err.message || err); - } - } papi.destroy(); } } diff --git a/examples/papi/s3-lifecycle.js b/examples/papi/s3-lifecycle.js index 3ae7ac5f..e3a67116 100644 --- a/examples/papi/s3-lifecycle.js +++ b/examples/papi/s3-lifecycle.js @@ -2,8 +2,8 @@ * S3 Registry lifecycle example (pallet-s3-registry). * * Walks the full S3-style object workflow on Layer 1: - * create_s3_bucket_with_storage (atomic: Layer 0 bucket + agreement request) - * -> provider accepts agreement + * negotiate provider-signed agreement terms over HTTP + * -> create_s3_bucket (atomic: Layer 0 bucket + primary agreement + S3 bucket) * -> upload chunks to the provider (HTTP) * -> put_object_metadata for each object (CID + size + content-type) * -> copy_object_metadata @@ -21,21 +21,19 @@ import assert from "node:assert"; import { copyObjectMetadata, - createS3BucketWithStorage, + createS3Bucket, deleteObjectMetadata, deleteS3Bucket, + negotiateTerms, putChunk, putObjectMetadata, } from "./api.js"; import { connect, ensureProviderRegistered, - ensureSoleAcceptingProvider, makeSigner, parseProviderClientArgs, - sameAddress, toHex, - waitForAgreementAcceptance, waitForBlockProduction, waitForChainReady, waitForNextBlock, @@ -91,43 +89,36 @@ async function main() { await waitForBlockProduction(api); await waitForNextBlock(papi); - let restoreOthers = null; try { console.log("\n=== Step 1: Ensure provider is ready ==="); await ensureProviderRegistered(api, provider, PROVIDER_URL); - // create_s3_bucket_with_storage picks query_available_providers[0], - // which iterates in storage-hash order. With multiple registered - // providers (CI registers //Alice and //Charlie) the match is - // non-deterministic — silence the others so the demo always matches - // the one whose HTTP endpoint we'll talk to. - restoreOthers = await ensureSoleAcceptingProvider(api, provider); - - console.log("\n=== Step 2: create_s3_bucket_with_storage ==="); - const maxCapacity = 1_048_576n; // 1 MiB + + console.log("\n=== Step 2: Negotiate signed agreement terms ==="); + const maxBytes = 1_048_576; // 1 MiB const duration = 100; - const { s3BucketId, layer0BucketId, matchedProvider } = - await createS3BucketWithStorage(api, client, BUCKET_NAME, { - max_capacity: maxCapacity, - duration, - max_payment: maxCapacity * BigInt(duration) * 2n, - }); + const signed = await negotiateTerms(PROVIDER_URL, { + owner: client.address, + max_bytes: maxBytes, + duration, + price_per_byte: 0, + replica_params: null, + }); + console.log( + " Provider signed terms: nonce=%s, valid_until=%s", + signed.terms.nonce, + signed.terms.valid_until + ); + + console.log("\n=== Step 3: create_s3_bucket ==="); + const { s3BucketId, layer0BucketId } = await createS3Bucket( + api, + client, + BUCKET_NAME, + provider, + signed + ); console.log(" s3_bucket_id =", s3BucketId); console.log(" layer0_bucket_id =", layer0BucketId); - console.log(" matched provider =", matchedProvider); - if (!sameAddress(matchedProvider, provider.address)) { - throw new Error( - `create_s3_bucket_with_storage matched ${matchedProvider}, expected ${provider.address}. ` + - `The provider node at ${PROVIDER_URL} can only accept for ${PROVIDER_SEED}.` - ); - } - - console.log("\n=== Step 3: Wait for provider to auto-accept agreement ==="); - // The provider node's agreement_coordinator polls every ~6s and accepts - // pending requests automatically. Don't try to submit accept_agreement - // ourselves — that races the coordinator and intermittently fails with - // AgreementRequestNotFound when the coordinator wins. - await waitForAgreementAcceptance(api, provider.address, layer0BucketId); - console.log(" Agreement accepted by", provider.address); console.log("\n=== Step 4: Upload two objects to the provider ==="); const obj1 = await putChunk( @@ -195,13 +186,6 @@ async function main() { if (err.stack) console.error(err.stack); process.exitCode = 1; } finally { - if (restoreOthers) { - try { - await restoreOthers(); - } catch (err) { - console.error("WARN: failed to restore other providers:", err.message || err); - } - } papi.destroy(); } } From ef2f8ae624e4d22c46b7e88e575e1b4d3f5982b0 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:56:20 +0700 Subject: [PATCH 22/44] feat: adpt examples to new agreement flow --- examples/papi/bucket-membership.js | 70 ++++++++++++++++++++-------- examples/papi/bucket-with-storage.js | 61 +++++++++++------------- examples/papi/checkpoint-missed.js | 26 +++++------ examples/papi/checkpoint-rewards.js | 37 +++++++-------- 4 files changed, 106 insertions(+), 88 deletions(-) diff --git a/examples/papi/bucket-membership.js b/examples/papi/bucket-membership.js index a6129342..eaf2f8d1 100644 --- a/examples/papi/bucket-membership.js +++ b/examples/papi/bucket-membership.js @@ -6,20 +6,29 @@ * the Writer to Admin, then remove the Reader. After each change the * bucket members are printed so the effect is visible. * - * This example is pure on-chain — no provider node, uploads, or - * agreements are involved. + * Buckets can no longer be created without a storage agreement, so the + * provider must be running to sign the agreement terms even though no + * uploads happen here. * * Prerequisites: * - Parachain running at ws://127.0.0.1:2222 + * - Provider node running (this script will register/configure //Alice if needed) * - Descriptors generated: npm run papi:generate * - * Usage: node bucket-membership.js [chain_ws] [admin_seed] [writer_seed] [reader_seed] + * Usage: node bucket-membership.js [chain_ws] [provider_url] [provider_seed] [admin_seed] [writer_seed] [reader_seed] */ import assert from "node:assert"; -import { createBucket, removeMember, setMember } from "./api.js"; +import { + establishStorageAgreement, + negotiateTerms, + removeMember, + setMember, +} from "./api.js"; import { connect, + ensureProviderRegistered, + ensureSoleAcceptingProvider, makeSigner, printBucketMembers, waitForBlockProduction, @@ -28,9 +37,11 @@ import { } from "./common.js"; const CHAIN_WS = process.argv[2] || "ws://127.0.0.1:2222"; -const ADMIN_SEED = process.argv[3] || "//Alice"; -const WRITER_SEED = process.argv[4] || "//Bob"; -const READER_SEED = process.argv[5] || "//Charlie"; +const PROVIDER_URL = process.argv[3] || "http://127.0.0.1:3333"; +const PROVIDER_SEED = process.argv[4] || "//Alice"; +const ADMIN_SEED = process.argv[5] || "//Bob"; +const WRITER_SEED = process.argv[6] || "//Charlie"; +const READER_SEED = process.argv[7] || "//Dave"; async function verifyReverseIndex(api, member, bucketId, shouldContain) { const buckets = await api.query.StorageProvider.MemberBuckets.getValue( @@ -46,14 +57,16 @@ async function verifyReverseIndex(api, member, bucketId, shouldContain) { } async function main() { + const provider = makeSigner(PROVIDER_SEED); const admin = makeSigner(ADMIN_SEED); const writer = makeSigner(WRITER_SEED); const reader = makeSigner(READER_SEED); - console.log("Chain:", CHAIN_WS); - console.log("Admin (%s) => %s", ADMIN_SEED, admin.address); - console.log("Writer (%s) => %s", WRITER_SEED, writer.address); - console.log("Reader (%s) => %s", READER_SEED, reader.address); + console.log("Chain:", CHAIN_WS, " Provider HTTP:", PROVIDER_URL); + console.log("Provider (%s) => %s", PROVIDER_SEED, provider.address); + console.log("Admin (%s) => %s", ADMIN_SEED, admin.address); + console.log("Writer (%s) => %s", WRITER_SEED, writer.address); + console.log("Reader (%s) => %s", READER_SEED, reader.address); const { papi, api } = await connect(CHAIN_WS); await waitForChainReady(api); @@ -61,28 +74,47 @@ async function main() { await waitForNextBlock(papi); try { - console.log("\n=== Step 1: Create bucket ==="); - const bucketId = await createBucket(api, admin); + console.log("\n=== Step 1: Ensure provider is ready ==="); + await ensureProviderRegistered(api, provider, PROVIDER_URL); + // Silence any other accepting dev providers (CI registers more than one) + // so the demo is deterministic. + + console.log("\n=== Step 2: Negotiate signed agreement terms ==="); + const signed = await negotiateTerms(PROVIDER_URL, { + owner: admin.address, + max_bytes: 1_048_576, // 1 MiB + duration: 100, + price_per_byte: 1, + replica_params: null, + }); + console.log( + " Provider signed terms: nonce=%s, valid_until=%s", + signed.terms.nonce, + signed.terms.valid_until + ); + + console.log("\n=== Step 3: Open bucket via establish_storage_agreement ==="); + const bucketId = await establishStorageAgreement(api, admin, provider, signed); console.log(" Bucket created: id=%s", bucketId); await printBucketMembers(api, bucketId, "after create"); - console.log("\n=== Step 2: Add Writer ==="); + console.log("\n=== Step 4: Add Writer ==="); await setMember(api, admin, bucketId, writer, "Writer"); await printBucketMembers(api, bucketId, "after add Writer"); - console.log("\n=== Step 3: Add Reader ==="); + console.log("\n=== Step 5: Add Reader ==="); await setMember(api, admin, bucketId, reader, "Reader"); await printBucketMembers(api, bucketId, "after add Reader"); - console.log("\n=== Step 4: Promote Writer -> Admin ==="); + console.log("\n=== Step 6: Promote Writer -> Admin ==="); await setMember(api, admin, bucketId, writer, "Admin"); await printBucketMembers(api, bucketId, "after promote"); - console.log("\n=== Step 5: Remove Reader ==="); + console.log("\n=== Step 7: Remove Reader ==="); await removeMember(api, admin, bucketId, reader); await printBucketMembers(api, bucketId, "after remove"); - console.log("\n=== Step 6: Verify reverse index ==="); + console.log("\n=== Step 8: Verify reverse index ==="); await verifyReverseIndex(api, writer, bucketId, true); await verifyReverseIndex(api, reader, bucketId, false); console.log("PASSED: ACL transitions applied as expected"); @@ -95,4 +127,4 @@ async function main() { } } -main().then(() => console.log("\n=== Done ===")); +main().then(() => console.log("\n=== Done ===")); \ No newline at end of file diff --git a/examples/papi/bucket-with-storage.js b/examples/papi/bucket-with-storage.js index 997e7fec..ae49c013 100644 --- a/examples/papi/bucket-with-storage.js +++ b/examples/papi/bucket-with-storage.js @@ -1,27 +1,25 @@ /** - * Atomic bucket-and-agreement quickstart for pallet-storage-provider. + * Bucket-and-agreement quickstart for pallet-storage-provider. * - * full-flow.js sets up a bucket and agreement step-by-step (create_bucket -> - * request_primary_agreement -> accept_agreement). This example uses the - * shortcut extrinsic create_bucket_with_storage, which performs all three in - * one transaction by auto-matching a provider that meets the requested - * price / duration / capacity. - * - * The script then uploads a single chunk, submits a checkpoint, and finally - * calls freeze_bucket — which becomes possible once a snapshot exists. + * Walks the minimum atomic flow: negotiate provider-signed terms over + * HTTP, redeem them via establish_storage_agreement (which opens the + * bucket + primary agreement in one tx), upload a single chunk, submit + * a checkpoint, then freeze the bucket — which only becomes possible + * once a snapshot exists. * * Prerequisites: * - Parachain at ws://127.0.0.1:2222 * - Provider node running and registered as //Alice with accepting_primary=true - * (run full-flow.js once to set that up, or this script will do it) + * (this script will register/configure //Alice if needed) * * Usage: node bucket-with-storage.js [chain_ws] [provider_url] [provider_seed] [client_seed] */ import { - createBucketWithStorage, + establishStorageAgreement, fetchCheckpointSignature, freezeBucket, + negotiateTerms, submitClientCheckpoint, uploadChunk, } from "./api.js"; @@ -31,7 +29,6 @@ import { ensureSoleAcceptingProvider, makeSigner, parseProviderClientArgs, - sameAddress, waitForBlockProduction, waitForChainReady, waitForNextBlock, @@ -69,27 +66,25 @@ async function main() { // deterministic. restoreOthers = await ensureSoleAcceptingProvider(api, provider); - console.log("\n=== Step 2: create_bucket_with_storage (atomic) ==="); - const { bucketId, matchedProvider, expiresAt } = await createBucketWithStorage( - api, - client, - { - max_bytes: 1_048_576n, - duration: 50, - max_price_per_byte: 10n, - } + console.log("\n=== Step 2: Negotiate signed agreement terms ==="); + const signed = await negotiateTerms(PROVIDER_URL, { + owner: client.address, + max_bytes: 1_048_576, // 1 MiB + duration: 50, + price_per_byte: 0, + replica_params: null, + }); + console.log( + " Provider signed terms: nonce=%s, valid_until=%s", + signed.terms.nonce, + signed.terms.valid_until ); - console.log(" bucket_id =", bucketId); - console.log(" matched provider =", matchedProvider); - console.log(" expires_at =", expiresAt); - if (!sameAddress(matchedProvider, provider.address)) { - throw new Error( - `create_bucket_with_storage matched ${matchedProvider}, expected ${provider.address}. ` + - `The provider node at ${PROVIDER_URL} can only sign for ${PROVIDER_SEED}.` - ); - } - console.log("\n=== Step 3: Upload one chunk ==="); + console.log("\n=== Step 3: establish_storage_agreement (atomic) ==="); + const bucketId = await establishStorageAgreement(api, client, provider, signed); + console.log(" bucket_id =", bucketId); + + console.log("\n=== Step 4: Upload one chunk ==="); const { data, commit } = await uploadChunk( PROVIDER_URL, bucketId, @@ -97,12 +92,12 @@ async function main() { ); console.log(" uploaded %d bytes, mmr_root=%s", data.length, commit.mmr_root); - console.log("\n=== Step 4: Submit checkpoint ==="); + console.log("\n=== Step 5: Submit checkpoint ==="); const ck = await fetchCheckpointSignature(PROVIDER_URL, bucketId); await submitClientCheckpoint(api, client, provider, bucketId, ck); console.log(" Checkpoint submitted (leaf_count=%s)", ck.leaf_count); - console.log("\n=== Step 5: Freeze bucket (now possible) ==="); + console.log("\n=== Step 6: Freeze bucket (now possible) ==="); const frozen = await freezeBucket(api, client, bucketId); console.log(" BucketFrozen at start_seq=%s", frozen.frozen_start_seq); diff --git a/examples/papi/checkpoint-missed.js b/examples/papi/checkpoint-missed.js index 67262e20..b927bfbd 100644 --- a/examples/papi/checkpoint-missed.js +++ b/examples/papi/checkpoint-missed.js @@ -23,9 +23,9 @@ import assert from "node:assert"; import { configureCheckpointWindow, - createBucket, + establishStorageAgreement, + negotiateTerms, reportMissedCheckpoint, - requestPrimaryAgreement, } from "./api.js"; import { connect, @@ -34,7 +34,6 @@ import { makeSigner, parseProviderClientArgs, sameAddress, - waitForAgreementAcceptance, waitForBlock, waitForBlockProduction, waitForChainReady, @@ -73,23 +72,20 @@ async function main() { await ensureProviderRegistered(api, provider, PROVIDER_URL); restoreOthers = await ensureSoleAcceptingProvider(api, provider); - const bucketId = await createBucket(api, client); - console.log(" Bucket created: id=%s", bucketId); - - const maxBytes = 1_048_576n; - const duration = 200; - await requestPrimaryAgreement(api, client, provider, bucketId, { - max_bytes: maxBytes, - duration, - max_payment: maxBytes * BigInt(duration) * 2n, + const signed = await negotiateTerms(PROVIDER_URL, { + owner: client.address, + max_bytes: 1_048_576, // 1 MiB + duration: 200, + price_per_byte: 1, + replica_params: null, }); - await waitForAgreementAcceptance(api, provider.address, bucketId); - console.log(" Agreement accepted"); + const bucketId = await establishStorageAgreement(api, client, provider, signed); + console.log(" Bucket + agreement opened: id=%s", bucketId); const bucket = await api.query.StorageProvider.Buckets.getValue(bucketId); assert.ok( bucket.primary_providers.some((p) => sameAddress(p, provider.address)), - "Provider should be primary after accept" + "Provider should be primary after establish_storage_agreement" ); console.log("\n=== Step 2: configure_checkpoint_window (tight) ==="); diff --git a/examples/papi/checkpoint-rewards.js b/examples/papi/checkpoint-rewards.js index fb9aa063..19c933e8 100644 --- a/examples/papi/checkpoint-rewards.js +++ b/examples/papi/checkpoint-rewards.js @@ -23,10 +23,10 @@ import assert from "node:assert"; import { claimCheckpointRewards, configureCheckpointWindow, - createBucket, + establishStorageAgreement, fetchCheckpointDuty, fundCheckpointPool, - requestPrimaryAgreement, + negotiateTerms, signCheckpointProposal, submitProviderCheckpoint, uploadChunk, @@ -38,7 +38,6 @@ import { makeSigner, parseProviderClientArgs, sameAddress, - waitForAgreementAcceptance, waitForBlock, waitForBlockProduction, waitForChainReady, @@ -175,20 +174,16 @@ async function main() { await ensureProviderRegistered(api, provider, PROVIDER_URL); restoreOthers = await ensureSoleAcceptingProvider(api, provider); - console.log("\n=== Step 2: Create bucket (client = admin) ==="); - const bucketId = await createBucket(api, client); - console.log(" Bucket created: id=%s", bucketId); - - console.log("\n=== Step 3: Open agreement so the provider becomes primary ==="); - const maxBytes = 1_048_576n; - const duration = 200; - await requestPrimaryAgreement(api, client, provider, bucketId, { - max_bytes: maxBytes, - duration, - max_payment: maxBytes * BigInt(duration) * 2n, + console.log("\n=== Step 2: Negotiate + establish_storage_agreement (atomic) ==="); + const signed = await negotiateTerms(PROVIDER_URL, { + owner: client.address, + max_bytes: 1_048_576, // 1 MiB + duration: 200, + price_per_byte: 1, + replica_params: null, }); - await waitForAgreementAcceptance(api, provider.address, bucketId); - console.log(" Agreement accepted (auto by provider node)"); + const bucketId = await establishStorageAgreement(api, client, provider, signed); + console.log(" Bucket + agreement opened: id=%s", bucketId); const bucket = await api.query.StorageProvider.Buckets.getValue(bucketId); assert.ok( @@ -196,7 +191,7 @@ async function main() { "Provider should be in primary_providers after agreement" ); - console.log("\n=== Step 4: Upload data so the MMR has something to commit ==="); + console.log("\n=== Step 3: Upload data so the MMR has something to commit ==="); const { data, commit } = await uploadChunk( PROVIDER_URL, bucketId, @@ -204,7 +199,7 @@ async function main() { ); console.log(" Uploaded %d bytes, mmr_root=%s", data.length, commit.mmr_root); - console.log("\n=== Step 5: configure_checkpoint_window ==="); + console.log("\n=== Step 4: configure_checkpoint_window ==="); const cfgEvent = await configureCheckpointWindow(api, client, bucketId, { interval: WINDOW_INTERVAL, gracePeriod: WINDOW_GRACE, @@ -216,7 +211,7 @@ async function main() { cfgEvent.enabled ); - console.log("\n=== Step 6: fund_checkpoint_pool ==="); + console.log("\n=== Step 5: fund_checkpoint_pool ==="); const fundEvent = await fundCheckpointPool(api, client, bucketId, POOL_AMOUNT); console.log( " Pool funded by %s with %s units", @@ -232,10 +227,10 @@ async function main() { `Pool balance ${balance} < funded amount ${POOL_AMOUNT}` ); - console.log("\n=== Step 7: provider_checkpoint (autonomous) ==="); + console.log("\n=== Step 6: provider_checkpoint (autonomous) ==="); const reward = await runProviderCheckpoint(api, papi, provider, bucketId); - console.log("\n=== Step 8: claim_checkpoint_rewards ==="); + console.log("\n=== Step 7: claim_checkpoint_rewards ==="); await claimAndVerify(api, provider, bucketId, reward); console.log("\nPASSED: provider-initiated checkpoint reward cycle complete"); From 522c53cf088b2c1b5bd92370f90337865c224210 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:48:42 +0700 Subject: [PATCH 23/44] refactor(provider-node): drop disk-backed nonce file, bootstrap from chain hwm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The on-chain sliding replay window (ProviderReplayState) is authoritative, so the local nonce file was redundant for correctness — it only avoided reissuing still-in-flight nonces across a restart, a liveness nicety not worth the extra failure surface and per-negotiate fs write. NonceCounter is now a pure in-memory monotonic counter: - remove path/open()/persist(); rename in_memory() -> new() - next() is just fetch_add; bootstrap_from_hwm() no longer writes - setup_nonce_counter() drops the StorageMode/nonce-path branch and just bootstraps from the on-chain hwm (falling back to 0 if unreachable) Also picks up rustfmt cleanups in agreement.rs / agreement_term.rs. --- primitives/src/agreement_term.rs | 2 - provider-node/src/command.rs | 33 +++-------- provider-node/src/negotiate.rs | 94 ++++++++------------------------ 3 files changed, 33 insertions(+), 96 deletions(-) diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs index 445dda4c..3815b69c 100644 --- a/primitives/src/agreement_term.rs +++ b/primitives/src/agreement_term.rs @@ -12,7 +12,6 @@ use core::fmt::Debug; use scale_info::TypeInfo; /// Off-chain quote signed by the provider and redeemed on-chain by the owner. -/// #[derive( Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, )] @@ -39,7 +38,6 @@ pub struct AgreementTerms { } /// Replica terms -/// #[derive( Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, )] diff --git a/provider-node/src/command.rs b/provider-node/src/command.rs index 8c7b3db6..96b6be8f 100644 --- a/provider-node/src/command.rs +++ b/provider-node/src/command.rs @@ -204,34 +204,19 @@ async fn start_replica_sync_coordinator( } } -/// Open the persistent nonce counter and bootstrap it from the chain's -/// `ProviderReplayState.hwm`. Default fallback to local storage, starting from 0 +/// Create the in-memory nonce counter and bootstrap it from the chain's +/// `ProviderReplayState.hwm`. The chain is the source of truth, so there +/// is nothing to persist locally. async fn setup_nonce_counter( cli: &Cli, provider_id: &str, ) -> Result, Box> { - let counter = match cli.storage.storage_mode { - StorageMode::Inmemory => NonceCounter::in_memory(0), - StorageMode::Disk => { - let nonce_path = cli.storage.storage_path.join("nonce"); - if let Some(parent) = nonce_path.parent() { - std::fs::create_dir_all(parent).map_err(|e| { - format!( - "failed to create nonce-counter parent dir {:?}: {}", - parent, e, - ) - })?; - } - NonceCounter::open(nonce_path.clone()) - .map_err(|e| format!("failed to open nonce counter at {:?}: {}", nonce_path, e))? - } - }; // Start the `nonce` from 1. - counter.bootstrap_from_hwm(0); + let counter = NonceCounter::new(1); - // Otherwise, bootstrap from on-chain hwm. Best-effort: if the chain isn't - // reachable yet, fall back to the local counter — the on-chain - // replay window will reject any out-of-range reissues anyway. + // Bootstrap from on-chain hwm. Best-effort: if the chain isn't + // reachable yet, start from 0 — the on-chain replay window will + // reject any out-of-range reissues anyway. let provider_account = sp_runtime::AccountId32::from_str(provider_id) .map_err(|e| format!("invalid provider SS58: {e:?}"))?; match storage_client::ProviderClient::fetch_replay_hwm(&cli.rpc.chain_rpc, &provider_account) @@ -247,13 +232,13 @@ async fn setup_nonce_counter( } Ok(None) => { tracing::info!( - "No on-chain replay state for provider {} yet; starting nonce counter from local storage", + "No on-chain replay state for provider {} yet; starting nonce counter from 0", provider_id, ); } Err(e) => { tracing::warn!( - "Failed to bootstrap nonce counter from chain: {}; falling back to local storage", + "Failed to bootstrap nonce counter from chain: {}; starting nonce counter from 0", e, ); } diff --git a/provider-node/src/negotiate.rs b/provider-node/src/negotiate.rs index e94677ec..28b557e0 100644 --- a/provider-node/src/negotiate.rs +++ b/provider-node/src/negotiate.rs @@ -3,11 +3,11 @@ //! Bucket owners ask the provider node for signed terms via //! `POST /negotiate`. The provider node: //! -//! 1. Allocates a fresh nonce from a persistent monotonic counter +//! 1. Allocates a fresh nonce from an in-memory monotonic counter //! ([`NonceCounter`]). The counter is initialized at startup from the -//! chain's `ProviderReplayState.hwm + 1`, so duplicates can't survive a -//! restart that lost the local file (the on-chain replay window will -//! reject them). +//! chain's `ProviderReplayState.hwm + 1`, so a restart can't reissue a +//! nonce the chain already accepted (the on-chain replay window is +//! authoritative and rejects any out-of-range reuse). //! 2. Builds [`AgreementTerms`] from the request, the provider's current //! `price_per_byte` setting (read from chain), and //! `valid_until = current_block + valid_until_offset`. @@ -17,59 +17,42 @@ use codec::Encode; use sp_core::Pair; use sp_runtime::MultiSignature; -use std::fs; -use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; // Wire types are shared with the SDK so client + server agree on serde shape. pub use storage_client::agreement::{AgreementTermsOf, NegotiateRequest, SignedTerms}; -/// Persistent monotonic nonce counter for provider-signed terms. +/// In-memory monotonic nonce counter for provider-signed terms. /// -/// Nonces are atomically allocated via [`Self::next`]. Each allocation -/// writes the new value to `path` synchronously so a crash mid-handler -/// can't reissue the same nonce. +/// Nonces are atomically allocated via [`Self::next`]. There is no local +/// persistence: at startup the caller reconciles against the chain by +/// calling [`Self::bootstrap_from_hwm`] with the provider's on-chain +/// `hwm`, so the counter resumes at `hwm + 1`. This: /// -/// At startup, the caller should reconcile against the chain by calling -/// [`Self::bootstrap_from_hwm`] with the provider's on-chain `hwm`. The -/// counter then resumes at `max(local, hwm + 1)`, which: -/// -/// * survives a restart that lost the local file (uses chain hwm); -/// * survives a restart where the chain advanced past our local view +/// * survives a restart (the chain hwm is the source of truth); +/// * survives a restart where the chain advanced past our last view /// (e.g. a parallel quote was redeemed elsewhere) — we skip past it /// rather than reissue. /// /// Gap-skipping is fine: unused nonces just expire from the replay -/// window without effect. +/// window without effect. The on-chain replay window is authoritative +/// and rejects any out-of-range reuse, so a missed nonce can never lead +/// to a double redemption. #[derive(Debug)] pub struct NonceCounter { counter: AtomicU64, - path: Option, } impl NonceCounter { - /// In-memory counter (testing). No persistence. - pub fn in_memory(start: u64) -> Self { + /// Create a counter starting at `start`. In normal operation the + /// caller follows up with [`Self::bootstrap_from_hwm`] to align with + /// the chain. + pub fn new(start: u64) -> Self { Self { counter: AtomicU64::new(start), - path: None, } } - /// Counter backed by a file. Reads the existing value if present, - /// otherwise starts at 0. - pub fn open(path: PathBuf) -> std::io::Result { - let start = match fs::read_to_string(&path) { - Ok(s) => s.trim().parse::().unwrap_or(0), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => 0, - Err(e) => return Err(e), - }; - Ok(Self { - counter: AtomicU64::new(start), - path: Some(path), - }) - } - /// Advance the counter to at least `hwm + 1`. Idempotent — only /// advances forward. pub fn bootstrap_from_hwm(&self, hwm: u64) { @@ -88,27 +71,12 @@ impl NonceCounter { Err(observed) => current = observed, } } - self.persist(); } - /// Allocate the next nonce. Atomic + persistent: even concurrent - /// callers each get a distinct value, and the on-disk file always - /// trails (or matches) the highest issued nonce. + /// Allocate the next nonce. Atomic: concurrent callers each get a + /// distinct value. pub fn next(&self) -> u64 { - let n = self.counter.fetch_add(1, Ordering::SeqCst); - self.persist(); - n - } - - /// Best-effort persist of the *next* value to disk. Failures are - /// logged but don't fail the call — the chain's replay window is - /// authoritative anyway. - fn persist(&self) { - let Some(ref path) = self.path else { return }; - let next = self.counter.load(Ordering::SeqCst); - if let Err(e) = fs::write(path, next.to_string()) { - tracing::warn!("Failed to persist nonce counter to {:?}: {}", path, e); - } + self.counter.fetch_add(1, Ordering::SeqCst) } } @@ -127,8 +95,8 @@ mod tests { use super::*; #[test] - fn nonce_counter_in_memory_is_monotonic() { - let c = NonceCounter::in_memory(0); + fn nonce_counter_is_monotonic() { + let c = NonceCounter::new(0); assert_eq!(c.next(), 0); assert_eq!(c.next(), 1); assert_eq!(c.next(), 2); @@ -136,24 +104,10 @@ mod tests { #[test] fn bootstrap_from_hwm_only_advances() { - let c = NonceCounter::in_memory(10); + let c = NonceCounter::new(10); c.bootstrap_from_hwm(5); // lower than current — no-op assert_eq!(c.next(), 10); c.bootstrap_from_hwm(20); // higher — advance assert_eq!(c.next(), 21); } - - #[test] - fn persists_and_resumes_from_disk() { - let dir = std::env::temp_dir().join(format!("nonce-counter-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("nonce"); - let c = NonceCounter::open(path.clone()).unwrap(); - assert_eq!(c.next(), 0); - assert_eq!(c.next(), 1); - drop(c); - // Reopening should pick up where we left off. - let c2 = NonceCounter::open(path).unwrap(); - assert_eq!(c2.next(), 2); - } } From bf8fb7b4623bc236b3835fb2c09245e0ddbb9d1c Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:26:05 +0700 Subject: [PATCH 24/44] refactor: rename replay-window anchor hwm -> hsn (high sequence nonce) Renames the ReplayWindow anchor field and all its callers from `hwm` (high-water mark) to `hsn` (high sequence nonce), which more accurately describes that it tracks the highest accepted agreement-term nonce: - ReplayWindow.hwm -> hsn (+ doc/comment updates) - ProviderClient::fetch_replay_hwm -> fetch_replay_hsn - NonceCounter::bootstrap_from_hwm -> bootstrap_from_hsn - setup_nonce_counter starts the counter at new(1), dropping the redundant bootstrap_from_hwm(0) Pure rename + identifier change; no behavioral or SCALE-encoding change. --- client/src/provider.rs | 6 ++-- pallet/src/lib.rs | 2 +- pallet/src/tests.rs | 16 ++++----- primitives/src/provider_replay_state.rs | 44 ++++++++++++------------- provider-node/src/command.rs | 14 ++++---- provider-node/src/negotiate.rs | 22 ++++++------- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/client/src/provider.rs b/client/src/provider.rs index 372f753d..09091da6 100644 --- a/client/src/provider.rs +++ b/client/src/provider.rs @@ -301,10 +301,10 @@ impl ProviderClient { // Term Negotiation (off-chain) // ═════════════════════════════════════════════════════════════════════════ - /// Read a provider's on-chain `ProviderReplayState.hwm`. Returns + /// Read a provider's on-chain `ProviderReplayState.hsn`. Returns /// `Ok(None)` if the provider has no replay state yet (never signed /// any terms). - pub async fn fetch_replay_hwm( + pub async fn fetch_replay_hsn( chain_ws_url: &str, provider: &AccountId32, ) -> ClientResult> { @@ -325,7 +325,7 @@ impl ProviderClient { let decoded = thunk .to_value() .map_err(|e| ClientError::Chain(format!("Failed to decode replay state: {e}")))?; - Ok(named_field(&decoded, "hwm") + Ok(named_field(&decoded, "hsn") .and_then(|v| v.as_u128()) .map(|h| h as u64)) } diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index ea17a284..26a01a23 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -803,7 +803,7 @@ pub mod pallet { /// replay window. NonceAlreadyUsed, /// The terms' nonce is older than the provider's replay window - /// (distance from `hwm` ≥ [`storage_primitives::REPLAY_WINDOW_BITS`]). + /// (distance from `hsn` ≥ [`storage_primitives::REPLAY_WINDOW_BITS`]). NonceTooOld, /// The terms' declared owner does not match the extrinsic origin. TermsOwnerMismatch, diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index 51f4ad3e..9c7c1c96 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -942,7 +942,7 @@ mod establish_storage_agreement_tests { // Replay window now anchored at nonce 7. let window = ProviderReplayStates::::get(2); - assert_eq!(window.hwm, 7); + assert_eq!(window.hsn, 7); assert_eq!(window.bitmap[0] & 1, 1); }); } @@ -1199,7 +1199,7 @@ mod establish_storage_agreement_tests { #[test] fn accepts_nonce_at_window_edge_and_rejects_one_past() { - // After advancing hwm to 300, nonce 45 (distance 255) is still in + // After advancing hsn to 300, nonce 45 (distance 255) is still in // the window, but nonce 44 (distance 256) is one slot past it. new_test_ext().execute_with(|| { System::set_block_number(1); @@ -1214,7 +1214,7 @@ mod establish_storage_agreement_tests { advance, sig, )); - assert_eq!(ProviderReplayStates::::get(2).hwm, 300); + assert_eq!(ProviderReplayStates::::get(2).hsn, 300); // Distance == REPLAY_WINDOW_BITS - 1 ⇒ accepted. let edge_nonce = 300 - (REPLAY_WINDOW_BITS as u64 - 1); @@ -1293,9 +1293,9 @@ mod establish_storage_agreement_tests { )); } - // hwm follows the max nonce seen. + // hsn follows the max nonce seen. let window = ProviderReplayStates::::get(2); - assert_eq!(window.hwm, 10); + assert_eq!(window.hsn, 10); // Replays of any of those nonces are rejected. for nonce in [3u64, 7, 1, 10, 2] { @@ -1316,7 +1316,7 @@ mod establish_storage_agreement_tests { #[test] fn forward_jump_beyond_window_clears_old_bits() { - // Bitmap shift: when hwm jumps forward by >= REPLAY_WINDOW_BITS, + // Bitmap shift: when hsn jumps forward by >= REPLAY_WINDOW_BITS, // every previously-set bit drops off the window so prior nonces // are now NonceTooOld, not NonceAlreadyUsed. new_test_ext().execute_with(|| { @@ -1345,8 +1345,8 @@ mod establish_storage_agreement_tests { )); let window = ProviderReplayStates::::get(2); - assert_eq!(window.hwm, 10_000); - // Only the new hwm bit is set; everything else is zero. + assert_eq!(window.hsn, 10_000); + // Only the new hsn bit is set; everything else is zero. assert_eq!(window.bitmap[0], 0b0000_0001); for byte in &window.bitmap[1..] { assert_eq!(*byte, 0); diff --git a/primitives/src/provider_replay_state.rs b/primitives/src/provider_replay_state.rs index 80bf2fa3..81c26dc8 100644 --- a/primitives/src/provider_replay_state.rs +++ b/primitives/src/provider_replay_state.rs @@ -2,15 +2,15 @@ //! //! Each provider maintains a sliding window over the most recent //! [`REPLAY_WINDOW_BITS`] nonces it has issued. The window is anchored at the -//! highest nonce ever accepted (`hwm`) and tracks the inclusive range -//! `hwm - (REPLAY_WINDOW_BITS - 1) ..= hwm`. Nonces older than the window +//! highest sequence nonce ever accepted (`hsn`) and tracks the inclusive range +//! `hsn - (REPLAY_WINDOW_BITS - 1) ..= hsn`. Nonces older than the window //! are rejected outright; nonces inside the window are accepted at most //! once. //! //! # Bit layout //! -//! The LSB of `bitmap[0]` represents `hwm`, the next bit represents -//! `hwm - 1`, and so on. Advancing the window by `d` slots shifts the +//! The LSB of `bitmap[0]` represents `hsn`, the next bit represents +//! `hsn - 1`, and so on. Advancing the window by `d` slots shifts the //! bitmap left by `d` bits, dropping the oldest entries. use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; @@ -36,10 +36,10 @@ pub const REPLAY_WINDOW_BITS: u32 = 256; )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ReplayWindow { - /// Highest nonce ever accepted for this provider (window anchor). - pub hwm: u64, + /// Highest sequence nonce ever accepted for this provider (window anchor). + pub hsn: u64, /// 256-bit acceptance bitmap; bit `i` (counting from the LSB of - /// `bitmap[0]`) is set iff nonce `hwm - i` has been accepted. + /// `bitmap[0]`) is set iff nonce `hsn - i` has been accepted. pub bitmap: [u8; 32], } @@ -48,33 +48,33 @@ pub struct ReplayWindow { pub enum ReplayError { /// The nonce is inside the window and has already been marked as seen. AlreadyUsed, - /// The nonce is more than [`REPLAY_WINDOW_BITS`] slots behind `hwm`. + /// The nonce is more than [`REPLAY_WINDOW_BITS`] slots behind `hsn`. TooOld, } impl ReplayWindow { /// Records `nonce` as seen, advancing the window if necessary. /// - /// When `nonce > hwm`, the bitmap is shifted left by `nonce - hwm` and - /// `hwm` is updated; the new high-water mark is then marked at bit 0. - /// When `nonce <= hwm`, the bit at distance `hwm - nonce` is set. + /// When `nonce > hsn`, the bitmap is shifted left by `nonce - hsn` and + /// `hsn` is updated; the new high sequence nonce is then marked at bit 0. + /// When `nonce <= hsn`, the bit at distance `hsn - nonce` is set. /// /// # Errors /// /// * [`ReplayError::AlreadyUsed`] if the nonce is inside the window /// and its bit is already set. /// * [`ReplayError::TooOld`] if the nonce is more than - /// [`REPLAY_WINDOW_BITS`] slots behind `hwm`. + /// [`REPLAY_WINDOW_BITS`] slots behind `hsn`. pub fn try_accept(&mut self, nonce: u64) -> Result<(), ReplayError> { - if nonce > self.hwm { - let shift = nonce - self.hwm; + if nonce > self.hsn { + let shift = nonce - self.hsn; shift_left_le(&mut self.bitmap, shift); - self.hwm = nonce; + self.hsn = nonce; self.bitmap[0] |= 1; return Ok(()); } - let distance = self.hwm - nonce; + let distance = self.hsn - nonce; if distance >= REPLAY_WINDOW_BITS as u64 { return Err(ReplayError::TooOld); } @@ -127,10 +127,10 @@ mod tests { use super::*; #[test] - fn first_nonce_sets_hwm() { + fn first_nonce_sets_hsn() { let mut w = ReplayWindow::default(); assert!(w.try_accept(5).is_ok()); - assert_eq!(w.hwm, 5); + assert_eq!(w.hsn, 5); assert_eq!(w.bitmap[0] & 1, 1); } @@ -147,7 +147,7 @@ mod tests { for n in [3u64, 7, 1, 10, 2] { assert!(w.try_accept(n).is_ok(), "nonce {n} should be accepted"); } - assert_eq!(w.hwm, 10); + assert_eq!(w.hsn, 10); for n in [3u64, 7, 1, 10, 2] { assert_eq!(w.try_accept(n), Err(ReplayError::AlreadyUsed)); } @@ -177,7 +177,7 @@ mod tests { w.try_accept(5).unwrap(); w.try_accept(7).unwrap(); w.try_accept(1000).unwrap(); - assert_eq!(w.hwm, 1000); + assert_eq!(w.hsn, 1000); assert_eq!(w.bitmap[0], 1); for b in &w.bitmap[1..] { assert_eq!(*b, 0); @@ -189,11 +189,11 @@ mod tests { let mut w = ReplayWindow::default(); w.try_accept(100).unwrap(); w.try_accept(101).unwrap(); - // 101 is hwm; 100 sits at distance 1, so bits 0 and 1 of byte 0 are set. + // 101 is hsn; 100 sits at distance 1, so bits 0 and 1 of byte 0 are set. assert_eq!(w.bitmap[0] & 0b11, 0b11); w.try_accept(109).unwrap(); // After shifting left by 8, the bits previously at positions 0 and 1 - // land in byte 1 at positions 0 and 1; bit 0 of byte 0 marks the new hwm. + // land in byte 1 at positions 0 and 1; bit 0 of byte 0 marks the new hsn. assert_eq!(w.bitmap[0] & 1, 1); assert_eq!(w.bitmap[1] & 0b11, 0b11); } diff --git a/provider-node/src/command.rs b/provider-node/src/command.rs index 96b6be8f..5dd39a07 100644 --- a/provider-node/src/command.rs +++ b/provider-node/src/command.rs @@ -205,7 +205,7 @@ async fn start_replica_sync_coordinator( } /// Create the in-memory nonce counter and bootstrap it from the chain's -/// `ProviderReplayState.hwm`. The chain is the source of truth, so there +/// `ProviderReplayState.hsn`. The chain is the source of truth, so there /// is nothing to persist locally. async fn setup_nonce_counter( cli: &Cli, @@ -214,21 +214,21 @@ async fn setup_nonce_counter( // Start the `nonce` from 1. let counter = NonceCounter::new(1); - // Bootstrap from on-chain hwm. Best-effort: if the chain isn't + // Bootstrap from on-chain hsn. Best-effort: if the chain isn't // reachable yet, start from 0 — the on-chain replay window will // reject any out-of-range reissues anyway. let provider_account = sp_runtime::AccountId32::from_str(provider_id) .map_err(|e| format!("invalid provider SS58: {e:?}"))?; - match storage_client::ProviderClient::fetch_replay_hwm(&cli.rpc.chain_rpc, &provider_account) + match storage_client::ProviderClient::fetch_replay_hsn(&cli.rpc.chain_rpc, &provider_account) .await { - Ok(Some(hwm)) => { + Ok(Some(hsn)) => { tracing::info!( - "Bootstrapping nonce counter from on-chain hwm {} for provider {}", - hwm, + "Bootstrapping nonce counter from on-chain hsn {} for provider {}", + hsn, provider_id, ); - counter.bootstrap_from_hwm(hwm); + counter.bootstrap_from_hsn(hsn); } Ok(None) => { tracing::info!( diff --git a/provider-node/src/negotiate.rs b/provider-node/src/negotiate.rs index 28b557e0..34cd2a0e 100644 --- a/provider-node/src/negotiate.rs +++ b/provider-node/src/negotiate.rs @@ -5,7 +5,7 @@ //! //! 1. Allocates a fresh nonce from an in-memory monotonic counter //! ([`NonceCounter`]). The counter is initialized at startup from the -//! chain's `ProviderReplayState.hwm + 1`, so a restart can't reissue a +//! chain's `ProviderReplayState.hsn + 1`, so a restart can't reissue a //! nonce the chain already accepted (the on-chain replay window is //! authoritative and rejects any out-of-range reuse). //! 2. Builds [`AgreementTerms`] from the request, the provider's current @@ -26,10 +26,10 @@ pub use storage_client::agreement::{AgreementTermsOf, NegotiateRequest, SignedTe /// /// Nonces are atomically allocated via [`Self::next`]. There is no local /// persistence: at startup the caller reconciles against the chain by -/// calling [`Self::bootstrap_from_hwm`] with the provider's on-chain -/// `hwm`, so the counter resumes at `hwm + 1`. This: +/// calling [`Self::bootstrap_from_hsn`] with the provider's on-chain +/// `hsn`, so the counter resumes at `hsn + 1`. This: /// -/// * survives a restart (the chain hwm is the source of truth); +/// * survives a restart (the chain hsn is the source of truth); /// * survives a restart where the chain advanced past our last view /// (e.g. a parallel quote was redeemed elsewhere) — we skip past it /// rather than reissue. @@ -45,7 +45,7 @@ pub struct NonceCounter { impl NonceCounter { /// Create a counter starting at `start`. In normal operation the - /// caller follows up with [`Self::bootstrap_from_hwm`] to align with + /// caller follows up with [`Self::bootstrap_from_hsn`] to align with /// the chain. pub fn new(start: u64) -> Self { Self { @@ -53,10 +53,10 @@ impl NonceCounter { } } - /// Advance the counter to at least `hwm + 1`. Idempotent — only + /// Advance the counter to at least `hsn + 1`. Idempotent — only /// advances forward. - pub fn bootstrap_from_hwm(&self, hwm: u64) { - let target = hwm.saturating_add(1); + pub fn bootstrap_from_hsn(&self, hsn: u64) { + let target = hsn.saturating_add(1); // Standard CAS loop — bump only if our target is higher than // whatever is already there. let mut current = self.counter.load(Ordering::SeqCst); @@ -103,11 +103,11 @@ mod tests { } #[test] - fn bootstrap_from_hwm_only_advances() { + fn bootstrap_from_hsn_only_advances() { let c = NonceCounter::new(10); - c.bootstrap_from_hwm(5); // lower than current — no-op + c.bootstrap_from_hsn(5); // lower than current — no-op assert_eq!(c.next(), 10); - c.bootstrap_from_hwm(20); // higher — advance + c.bootstrap_from_hsn(20); // higher — advance assert_eq!(c.next(), 21); } } From 9dc759aaedaf7c7d2433ce49888c3a34033d83bb Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:38:33 +0700 Subject: [PATCH 25/44] feat(precompiles): move bucket/drive creation to provider-signed terms flow The pallets replaced open-ended bucket creation (create_bucket, create_bucket_with_storage, request_primary_agreement) with the negotiate-then-redeem flow, so the precompiles follow: - storage-provider: drop createBucket/createBucketWithStorage/ requestPrimaryAgreement; add establishStorageAgreement(provider, terms, signature) returning the new bucket id - s3-registry: createS3Bucket now redeems provider-signed terms; drop createS3BucketWithStorage - drive-registry: createDrive now takes (name, provider, terms, signature) AgreementTerms/ReplicaTerms cross the Solidity boundary as PrimitiveAgreementTerms/PrimitiveReplicaTerms structs declared in each interface (alloy::sol! cannot resolve Solidity imports, so the mirrors are per-file copies); the signature is the SCALE-encoded MultiSignature from the provider's /negotiate response. Example contracts and interface copies updated to match. --- Cargo.lock | 1 + examples/contracts/IDriveRegistry.sol | 46 +++++++-- examples/contracts/IS3Registry.sol | 49 +++++++--- examples/contracts/IWeb3Storage.sol | 76 +++++++++++--- examples/contracts/SharedTeamDrive.sol | 26 +++-- examples/contracts/StorageMarketplace.sol | 38 ++++--- examples/contracts/TokenGatedDrive.sol | 23 +++-- .../src/IDriveRegistry.sol | 46 +++++++-- .../drive-registry-precompile/src/lib.rs | 60 +++++++++--- precompiles/s3-registry-precompile/Cargo.toml | 2 + .../src/IS3Registry.sol | 49 +++++++--- precompiles/s3-registry-precompile/src/lib.rs | 83 +++++++++++----- .../src/IWeb3Storage.sol | 61 +++++++----- .../storage-provider-precompile/src/lib.rs | 98 +++++++++---------- 14 files changed, 456 insertions(+), 202 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d057952..6457cb6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5736,6 +5736,7 @@ dependencies = [ "parity-scale-codec", "sp-core", "sp-runtime", + "storage-primitives", "tracing", ] diff --git a/examples/contracts/IDriveRegistry.sol b/examples/contracts/IDriveRegistry.sol index f2a1dbaa..576583bd 100644 --- a/examples/contracts/IDriveRegistry.sol +++ b/examples/contracts/IDriveRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; /// @title IDriveRegistry /// @notice Solidity interface for the web3-storage `pallet_drive_registry` @@ -9,19 +9,49 @@ pragma solidity ^0.8.0; /// /// Role tags: 0 = Admin, 1 = Writer, 2 = Reader. interface IDriveRegistry { - /// Create a new drive (auto-allocates a Layer 0 bucket and selects providers). + // TODO: Find out way to make it re-useable + struct PrimitiveReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. + uint128 syncBalance; + /// Minimum blocks between sync confirmations the provider commits to. + uint32 minSyncInterval; + } + + struct PrimitiveAgreementTerms { + /// Owner bound by these terms (must be the caller's substrate-mapped + /// account at redemption). + bytes32 owner; + /// Storage quota committed by the provider, in bytes. + uint64 maxBytes; + /// Agreement duration in blocks from activation. + uint32 duration; + /// Price per byte per block locked at quote time. + uint128 pricePerByte; + /// Block number after which the quote is no longer redeemable. + uint32 validUntil; + /// Provider-chosen replay-protection nonce. + uint64 nonce; + /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). + bool hasReplicaParams; + /// Replica funding parameters; only read when `hasReplicaParams` is true. + PrimitiveReplicaTerms replicaParams; + } + + /// Create a new drive by redeeming provider-signed agreement terms: the + /// underlying Layer 0 bucket and primary agreement are opened atomically. /// /// - `name` may be empty (treated as `None`). - /// - `minProviders == 0` means "use runtime default"; any value > 0 is - /// forwarded as `Some(n)`. + /// - `terms` must match the SCALE payload the provider signed; + /// `terms.owner` must be the caller's substrate-mapped account. + /// - `signature` is the SCALE-encoded `MultiSignature` from the provider's + /// `/negotiate` response (variant byte + raw signature bytes). /// /// Returns the new drive id. function createDrive( string calldata name, - uint64 maxCapacity, - uint32 storagePeriod, - uint128 payment, - uint8 minProviders + bytes32 provider, + PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external returns (uint64 driveId); /// Delete a drive, refunding any remaining payment to the owner. diff --git a/examples/contracts/IS3Registry.sol b/examples/contracts/IS3Registry.sol index c295e9e9..439ebbcc 100644 --- a/examples/contracts/IS3Registry.sol +++ b/examples/contracts/IS3Registry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; /// @title IS3Registry /// @notice Solidity interface for the web3-storage `pallet_s3_registry` @@ -7,18 +7,45 @@ pragma solidity ^0.8.0; /// bucket owner; bucket names follow the S3 convention (3-63 chars, /// lowercase alphanumeric + hyphens). `cid` is a substrate `H256`. interface IS3Registry { - /// Create an S3 bucket (no storage agreement yet). - function createS3Bucket(string calldata name, uint32 minProviders) - external returns (uint64 s3BucketId); + // TODO: Find out way to make it re-useable + struct PrimitiveReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. + uint128 syncBalance; + /// Minimum blocks between sync confirmations the provider commits to. + uint32 minSyncInterval; + } - /// Create an S3 bucket and atomically open a primary storage agreement. - /// `msg.value` (substrate units, via NativeToEthRatio) funds the - /// contract's account so the pallet can reserve the payment. - function createS3BucketWithStorage( + struct PrimitiveAgreementTerms { + /// Owner bound by these terms (must be the caller's substrate-mapped + /// account at redemption). + bytes32 owner; + /// Storage quota committed by the provider, in bytes. + uint64 maxBytes; + /// Agreement duration in blocks from activation. + uint32 duration; + /// Price per byte per block locked at quote time. + uint128 pricePerByte; + /// Block number after which the quote is no longer redeemable. + uint32 validUntil; + /// Provider-chosen replay-protection nonce. + uint64 nonce; + /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). + bool hasReplicaParams; + /// Replica funding parameters; only read when `hasReplicaParams` is true. + PrimitiveReplicaTerms replicaParams; + } + + /// Create an S3 bucket by redeeming provider-signed agreement terms: the + /// underlying Layer 0 bucket and primary agreement are opened atomically. + /// `terms` must match the SCALE payload the provider signed; `terms.owner` + /// must be the caller's substrate-mapped account. `signature` is the + /// SCALE-encoded `MultiSignature` from the provider's `/negotiate` + /// response (variant byte + raw signature bytes). + function createS3Bucket( string calldata name, - uint64 maxCapacity, - uint32 duration, - uint128 maxPayment + bytes32 provider, + PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external returns (uint64 s3BucketId); /// Delete an empty bucket. Caller must be the owner. diff --git a/examples/contracts/IWeb3Storage.sol b/examples/contracts/IWeb3Storage.sol index 4a2a68dc..d5edfd62 100644 --- a/examples/contracts/IWeb3Storage.sol +++ b/examples/contracts/IWeb3Storage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; /// @title IWeb3Storage /// @notice ABI of the web3-storage precompile at @@ -12,36 +12,86 @@ pragma solidity ^0.8.0; /// /// Role tags: 0 = Admin, 1 = Writer, 2 = Reader. interface IWeb3Storage { - function createBucket(uint32 minProviders) external returns (uint64 bucketId); - function createBucketWithStorage( - uint64 maxBytes, - uint32 duration, - uint128 maxPricePerByte + // TODO: Find out way to make it re-useable + struct PrimitiveReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. + uint128 syncBalance; + /// Minimum blocks between sync confirmations the provider commits to. + uint32 minSyncInterval; + } + + struct PrimitiveAgreementTerms { + /// Owner bound by these terms (must be the caller's substrate-mapped + /// account at redemption). + bytes32 owner; + /// Storage quota committed by the provider, in bytes. + uint64 maxBytes; + /// Agreement duration in blocks from activation. + uint32 duration; + /// Price per byte per block locked at quote time. + uint128 pricePerByte; + /// Block number after which the quote is no longer redeemable. + uint32 validUntil; + /// Provider-chosen replay-protection nonce. + uint64 nonce; + /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). + bool hasReplicaParams; + /// Replica funding parameters; only read when `hasReplicaParams` is true. + PrimitiveReplicaTerms replicaParams; + } + + // --- Bucket lifecycle --------------------------------------------------- + + /// Redeem provider-signed agreement terms: create a bucket and open a + /// primary agreement atomically. `terms` must match the SCALE payload the + /// provider signed; `terms.owner` must be the caller's substrate-mapped + /// account. `signature` is the SCALE-encoded `MultiSignature` from the + /// provider's `/negotiate` response (variant byte + raw signature bytes). + /// Returns the new bucket id. + function establishStorageAgreement( + bytes32 provider, + PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external returns (uint64 bucketId); + + /// Freeze a bucket — append-only, irreversible. function freezeBucket(uint64 bucketId) external; + + // --- Membership --------------------------------------------------------- + + /// Add or update a bucket member. function setMember(uint64 bucketId, bytes32 member, uint8 role) external; + + /// Remove a member from a bucket. function removeMember(uint64 bucketId, bytes32 member) external; - function requestPrimaryAgreement( - uint64 bucketId, - bytes32 provider, - uint64 maxBytes, - uint32 duration, - uint128 maxPayment - ) external; + + // --- Agreement lifecycle ------------------------------------------------ + + /// Add funds and capacity to an existing agreement. function topUpAgreement( uint64 bucketId, bytes32 provider, uint64 additionalBytes, uint128 maxPayment ) external; + + /// Extend an existing agreement's duration. function extendAgreement( uint64 bucketId, bytes32 provider, uint32 additionalDuration, uint128 maxPayment ) external; + + /// End an agreement, paying the provider in full. function endAgreementPay(uint64 bucketId, bytes32 provider) external; + + /// End an agreement, burning `burnPercent` (0-100) and paying the rest. function endAgreementBurn(uint64 bucketId, bytes32 provider, uint8 burnPercent) external; + + // --- Challenges --------------------------------------------------------- + + /// Challenge a provider's checkpoint at a specific leaf/chunk. function challengeCheckpoint( uint64 bucketId, bytes32 provider, diff --git a/examples/contracts/SharedTeamDrive.sol b/examples/contracts/SharedTeamDrive.sol index 5b319a85..efdb9134 100644 --- a/examples/contracts/SharedTeamDrive.sol +++ b/examples/contracts/SharedTeamDrive.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; import "./IDriveRegistry.sol"; @@ -11,7 +11,9 @@ import "./IDriveRegistry.sol"; /// without crawling the bucket's membership list. /// /// Lifecycle: -/// 1. Whoever deploys + calls `createTeam` becomes the team admin. +/// 1. Whoever deploys + calls `createTeam` becomes the team admin. The +/// terms are negotiated off-chain with a provider (`POST /negotiate`) +/// using the *contract's* substrate-mapped account as `terms.owner`. /// 2. Admin can `invite(member, role)` and `kick(member)` — these update /// the contract-side `memberRole` map and forward to /// `DRIVE_REGISTRY.shareDrive` / `unshareDrive`. @@ -45,25 +47,19 @@ contract SharedTeamDrive { _; } - /// Create the team's drive. `msg.value` funds the agreement payment - /// reserve held by the contract's substrate-mapped account. + /// Create the team's drive by redeeming provider-signed terms + /// (`terms.owner` must be the contract's substrate-mapped account). + /// `msg.value` funds the agreement payment reserve held by that account. function createTeam( string calldata name, - uint64 maxCapacity, - uint32 storagePeriod, - uint128 payment, - uint8 minProviders + bytes32 provider, + IDriveRegistry.PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external payable returns (uint64) { require(admin == address(0), "team already created"); require(msg.value > 0, "must fund agreement"); admin = msg.sender; - driveId = DRIVE_REGISTRY.createDrive( - name, - maxCapacity, - storagePeriod, - payment, - minProviders - ); + driveId = DRIVE_REGISTRY.createDrive(name, provider, terms, signature); emit TeamCreated(msg.sender, driveId); return driveId; } diff --git a/examples/contracts/StorageMarketplace.sol b/examples/contracts/StorageMarketplace.sol index 1fc03ba8..04410fac 100644 --- a/examples/contracts/StorageMarketplace.sol +++ b/examples/contracts/StorageMarketplace.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; import "./IWeb3Storage.sol"; @@ -8,13 +8,17 @@ import "./IWeb3Storage.sol"; /// precompile to buy storage on behalf of its users. /// /// Flow: -/// 1. User calls `buyStorage{value: nativeAmount}(maxBytes, duration, maxPricePerByte)`. +/// 1. Off-chain, the user negotiates terms with a provider (`POST +/// /negotiate` on the provider node) using the *contract's* +/// substrate-mapped account as `terms.owner` — the contract is the +/// precompile caller, so the pallet binds the agreement to it. +/// 2. User calls `buyStorage{value: nativeAmount}(provider, terms, signature)`. /// `msg.value` funds the contract's substrate-mapped account; the -/// precompile then reserves the agreement payment from that balance. -/// 2. The precompile auto-matches a provider, opens a primary agreement, -/// and returns the new bucket id. The contract records the (user, -/// bucket, provider) triple so users can wind their agreement down -/// later without touching the chain directly. +/// precompile verifies the provider's signature, reserves the payment +/// from that balance, and atomically creates the bucket + primary +/// agreement, returning the new bucket id. The contract records the +/// (user, bucket) pair so users can wind their agreement down later +/// without touching the chain directly. /// 3. To wind down, the user calls `endMyAgreement(bucketId, provider)`. /// Only the original buyer can end their bucket's agreement. /// @@ -33,18 +37,20 @@ contract StorageMarketplace { event BucketBoughtFor(address indexed user, uint64 indexed bucketId); event AgreementEnded(address indexed user, uint64 indexed bucketId); - /// Buy storage on behalf of `msg.sender`. The contract becomes the - /// substrate-side bucket admin; per-user ownership is tracked here. + /// Buy storage on behalf of `msg.sender` by redeeming provider-signed + /// terms. The contract becomes the substrate-side bucket admin + /// (`terms.owner` must be its substrate-mapped account); per-user + /// ownership is tracked here. function buyStorage( - uint64 maxBytes, - uint32 duration, - uint128 maxPricePerByte + bytes32 provider, + IWeb3Storage.PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external payable returns (uint64 bucketId) { require(msg.value > 0, "msg.value must cover the agreement payment"); - bucketId = WEB3_STORAGE.createBucketWithStorage( - maxBytes, - duration, - maxPricePerByte + bucketId = WEB3_STORAGE.establishStorageAgreement( + provider, + terms, + signature ); bucketOwner[bucketId] = msg.sender; emit BucketBoughtFor(msg.sender, bucketId); diff --git a/examples/contracts/TokenGatedDrive.sol b/examples/contracts/TokenGatedDrive.sol index 5eb19d12..ad351aac 100644 --- a/examples/contracts/TokenGatedDrive.sol +++ b/examples/contracts/TokenGatedDrive.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; import "./IS3Registry.sol"; @@ -15,7 +15,9 @@ import "./IS3Registry.sol"; /// gating pattern, not ship a marketplace-ready NFT. /// /// Lifecycle: -/// 1. Deployer calls `initialize(name, maxCapacity, duration, maxPayment)` +/// 1. Deployer negotiates terms off-chain with a provider (`POST +/// /negotiate`, `terms.owner` = the contract's substrate-mapped +/// account), then calls `initialize(name, provider, terms, signature)` /// with `msg.value` funding the agreement reserve. Becomes the /// publisher. /// 2. Publisher calls `mint(buyer, key, cid, size, contentType)` per @@ -56,22 +58,19 @@ contract TokenGatedDrive { _; } - /// Bootstrap the bucket. One-shot. + /// Bootstrap the bucket by redeeming provider-signed terms + /// (`terms.owner` must be the contract's substrate-mapped account). + /// One-shot. function initialize( string calldata name, - uint64 maxCapacity, - uint32 duration, - uint128 maxPayment + bytes32 provider, + IS3Registry.PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external payable returns (uint64) { require(publisher == address(0), "already init"); require(msg.value > 0, "must fund agreement"); publisher = msg.sender; - s3BucketId = S3_REGISTRY.createS3BucketWithStorage( - name, - maxCapacity, - duration, - maxPayment - ); + s3BucketId = S3_REGISTRY.createS3Bucket(name, provider, terms, signature); emit Initialized(msg.sender, s3BucketId); return s3BucketId; } diff --git a/precompiles/drive-registry-precompile/src/IDriveRegistry.sol b/precompiles/drive-registry-precompile/src/IDriveRegistry.sol index f2a1dbaa..576583bd 100644 --- a/precompiles/drive-registry-precompile/src/IDriveRegistry.sol +++ b/precompiles/drive-registry-precompile/src/IDriveRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; /// @title IDriveRegistry /// @notice Solidity interface for the web3-storage `pallet_drive_registry` @@ -9,19 +9,49 @@ pragma solidity ^0.8.0; /// /// Role tags: 0 = Admin, 1 = Writer, 2 = Reader. interface IDriveRegistry { - /// Create a new drive (auto-allocates a Layer 0 bucket and selects providers). + // TODO: Find out way to make it re-useable + struct PrimitiveReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. + uint128 syncBalance; + /// Minimum blocks between sync confirmations the provider commits to. + uint32 minSyncInterval; + } + + struct PrimitiveAgreementTerms { + /// Owner bound by these terms (must be the caller's substrate-mapped + /// account at redemption). + bytes32 owner; + /// Storage quota committed by the provider, in bytes. + uint64 maxBytes; + /// Agreement duration in blocks from activation. + uint32 duration; + /// Price per byte per block locked at quote time. + uint128 pricePerByte; + /// Block number after which the quote is no longer redeemable. + uint32 validUntil; + /// Provider-chosen replay-protection nonce. + uint64 nonce; + /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). + bool hasReplicaParams; + /// Replica funding parameters; only read when `hasReplicaParams` is true. + PrimitiveReplicaTerms replicaParams; + } + + /// Create a new drive by redeeming provider-signed agreement terms: the + /// underlying Layer 0 bucket and primary agreement are opened atomically. /// /// - `name` may be empty (treated as `None`). - /// - `minProviders == 0` means "use runtime default"; any value > 0 is - /// forwarded as `Some(n)`. + /// - `terms` must match the SCALE payload the provider signed; + /// `terms.owner` must be the caller's substrate-mapped account. + /// - `signature` is the SCALE-encoded `MultiSignature` from the provider's + /// `/negotiate` response (variant byte + raw signature bytes). /// /// Returns the new drive id. function createDrive( string calldata name, - uint64 maxCapacity, - uint32 storagePeriod, - uint128 payment, - uint8 minProviders + bytes32 provider, + PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external returns (uint64 driveId); /// Delete a drive, refunding any remaining payment to the owner. diff --git a/precompiles/drive-registry-precompile/src/lib.rs b/precompiles/drive-registry-precompile/src/lib.rs index d3867d63..4f5e42fb 100644 --- a/precompiles/drive-registry-precompile/src/lib.rs +++ b/precompiles/drive-registry-precompile/src/lib.rs @@ -48,6 +48,33 @@ where }) } +/// Rebuild the pallet's [`AgreementTermsOf`](pallet_storage_provider::AgreementTermsOf) +/// from its Solidity mirror so the SCALE encoding matches the payload the +/// provider signed. +fn decode_terms( + terms: &IDriveRegistry::PrimitiveAgreementTerms, +) -> Result, Error> +where + T: pallet_storage_provider::Config, + BalanceOf: From, + BlockNumberFor: From, +{ + Ok(storage_primitives::AgreementTerms { + owner: decode_account::(&terms.owner.0)?, + max_bytes: terms.maxBytes, + duration: BlockNumberFor::::from(terms.duration), + price_per_byte: BalanceOf::::from(terms.pricePerByte), + valid_until: BlockNumberFor::::from(terms.validUntil), + nonce: terms.nonce, + replica_params: terms + .hasReplicaParams + .then(|| storage_primitives::ReplicaTerms { + sync_balance: BalanceOf::::from(terms.replicaParams.syncBalance), + min_sync_interval: BlockNumberFor::::from(terms.replicaParams.minSyncInterval), + }), + }) +} + fn decode_role(tag: u8) -> Result { match tag { 0 => Ok(Role::Admin), @@ -98,30 +125,35 @@ where match input { IDriveRegistryCalls::createDrive(IDriveRegistry::createDriveCall { name, - maxCapacity, - storagePeriod, - payment, - minProviders, + provider, + terms, + signature, }) => { env.charge(::WeightInfo::create_drive())?; - let drive_id = pallet_drive_registry::NextDriveId::::get(); + let provider = decode_account::(&provider.0)?; + let terms = decode_terms::(terms)?; + let sig = + sp_runtime::MultiSignature::decode(&mut signature.as_ref()).map_err(|e| { + revert( + &e, + "Invalid signature encoding: expected SCALE-encoded MultiSignature", + ) + })?; let name_opt = if name.is_empty() { None } else { Some(name.as_bytes().to_vec()) }; - let min_providers_opt = if *minProviders == 0 { - None - } else { - Some(*minProviders) - }; + // `NextDriveId` is incremented inside the extrinsic; capture + // the pre-dispatch value so we can return the id assigned to + // this call. + let drive_id = pallet_drive_registry::NextDriveId::::get(); pallet_drive_registry::Pallet::::create_drive( frame_origin, name_opt, - *maxCapacity, - BlockNumberFor::::from(*storagePeriod), - BalanceOf::::from(*payment), - min_providers_opt, + provider, + terms, + sig, ) .map_err(|e| revert(&e, "createDrive failed"))?; Ok(drive_id.abi_encode()) diff --git a/precompiles/s3-registry-precompile/Cargo.toml b/precompiles/s3-registry-precompile/Cargo.toml index d668fe0e..1a6f3c6a 100644 --- a/precompiles/s3-registry-precompile/Cargo.toml +++ b/precompiles/s3-registry-precompile/Cargo.toml @@ -16,6 +16,7 @@ pallet-s3-registry = { workspace = true } pallet-storage-provider = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } +storage-primitives = { workspace = true } tracing = { workspace = true } [features] @@ -29,6 +30,7 @@ std = [ "pallet-storage-provider/std", "sp-core/std", "sp-runtime/std", + "storage-primitives/std", "tracing/std", ] runtime-benchmarks = [ diff --git a/precompiles/s3-registry-precompile/src/IS3Registry.sol b/precompiles/s3-registry-precompile/src/IS3Registry.sol index c295e9e9..439ebbcc 100644 --- a/precompiles/s3-registry-precompile/src/IS3Registry.sol +++ b/precompiles/s3-registry-precompile/src/IS3Registry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; /// @title IS3Registry /// @notice Solidity interface for the web3-storage `pallet_s3_registry` @@ -7,18 +7,45 @@ pragma solidity ^0.8.0; /// bucket owner; bucket names follow the S3 convention (3-63 chars, /// lowercase alphanumeric + hyphens). `cid` is a substrate `H256`. interface IS3Registry { - /// Create an S3 bucket (no storage agreement yet). - function createS3Bucket(string calldata name, uint32 minProviders) - external returns (uint64 s3BucketId); + // TODO: Find out way to make it re-useable + struct PrimitiveReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. + uint128 syncBalance; + /// Minimum blocks between sync confirmations the provider commits to. + uint32 minSyncInterval; + } - /// Create an S3 bucket and atomically open a primary storage agreement. - /// `msg.value` (substrate units, via NativeToEthRatio) funds the - /// contract's account so the pallet can reserve the payment. - function createS3BucketWithStorage( + struct PrimitiveAgreementTerms { + /// Owner bound by these terms (must be the caller's substrate-mapped + /// account at redemption). + bytes32 owner; + /// Storage quota committed by the provider, in bytes. + uint64 maxBytes; + /// Agreement duration in blocks from activation. + uint32 duration; + /// Price per byte per block locked at quote time. + uint128 pricePerByte; + /// Block number after which the quote is no longer redeemable. + uint32 validUntil; + /// Provider-chosen replay-protection nonce. + uint64 nonce; + /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). + bool hasReplicaParams; + /// Replica funding parameters; only read when `hasReplicaParams` is true. + PrimitiveReplicaTerms replicaParams; + } + + /// Create an S3 bucket by redeeming provider-signed agreement terms: the + /// underlying Layer 0 bucket and primary agreement are opened atomically. + /// `terms` must match the SCALE payload the provider signed; `terms.owner` + /// must be the caller's substrate-mapped account. `signature` is the + /// SCALE-encoded `MultiSignature` from the provider's `/negotiate` + /// response (variant byte + raw signature bytes). + function createS3Bucket( string calldata name, - uint64 maxCapacity, - uint32 duration, - uint128 maxPayment + bytes32 provider, + PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external returns (uint64 s3BucketId); /// Delete an empty bucket. Caller must be the owner. diff --git a/precompiles/s3-registry-precompile/src/lib.rs b/precompiles/s3-registry-precompile/src/lib.rs index 59c2424b..bf14656e 100644 --- a/precompiles/s3-registry-precompile/src/lib.rs +++ b/precompiles/s3-registry-precompile/src/lib.rs @@ -11,6 +11,7 @@ extern crate alloc; use alloc::{vec, vec::Vec}; +use codec::Decode; use core::{fmt, marker::PhantomData, num::NonZero}; use frame_support::dispatch::RawOrigin; use frame_system::pallet_prelude::BlockNumberFor; @@ -36,6 +37,47 @@ fn revert(error: &impl fmt::Debug, message: &str) -> Error { Error::Revert(message.into()) } +/// Decode a Solidity `bytes32` as a substrate `AccountId`. Our runtimes use +/// `AccountId32`, whose SCALE encoding is the raw 32 bytes — `Decode` matches. +fn decode_account(bytes: &[u8; 32]) -> Result +where + T: frame_system::Config, +{ + T::AccountId::decode(&mut &bytes[..]).map_err(|e| { + revert( + &e, + "Invalid account encoding: expected 32-byte substrate AccountId", + ) + }) +} + +/// Rebuild the pallet's [`AgreementTermsOf`](pallet_storage_provider::AgreementTermsOf) +/// from its Solidity mirror so the SCALE encoding matches the payload the +/// provider signed. +fn decode_terms( + terms: &IS3Registry::PrimitiveAgreementTerms, +) -> Result, Error> +where + T: pallet_storage_provider::Config, + BalanceOf: From, + BlockNumberFor: From, +{ + Ok(storage_primitives::AgreementTerms { + owner: decode_account::(&terms.owner.0)?, + max_bytes: terms.maxBytes, + duration: BlockNumberFor::::from(terms.duration), + price_per_byte: BalanceOf::::from(terms.pricePerByte), + valid_until: BlockNumberFor::::from(terms.validUntil), + nonce: terms.nonce, + replica_params: terms + .hasReplicaParams + .then(|| storage_primitives::ReplicaTerms { + sync_balance: BalanceOf::::from(terms.replicaParams.syncBalance), + min_sync_interval: BlockNumberFor::::from(terms.replicaParams.minSyncInterval), + }), + }) +} + /// Precompile wrapping `pallet_s3_registry`'s public extrinsics. pub struct S3RegistryPrecompile(PhantomData); @@ -73,42 +115,37 @@ where match input { IS3RegistryCalls::createS3Bucket(IS3Registry::createS3BucketCall { name, - minProviders, + provider, + terms, + signature, }) => { env.charge( ::WeightInfo::create_s3_bucket(), )?; + let provider = decode_account::(&provider.0)?; + let terms = decode_terms::(terms)?; + let sig = + sp_runtime::MultiSignature::decode(&mut signature.as_ref()).map_err(|e| { + revert( + &e, + "Invalid signature encoding: expected SCALE-encoded MultiSignature", + ) + })?; + // `NextS3BucketId` is incremented inside the extrinsic; capture + // the pre-dispatch value so we can return the id assigned to + // this call. let s3_bucket_id = pallet_s3_registry::NextS3BucketId::::get(); pallet_s3_registry::Pallet::::create_s3_bucket( frame_origin, name.as_bytes().to_vec(), - *minProviders, + provider, + terms, + sig, ) .map_err(|e| revert(&e, "createS3Bucket failed"))?; Ok(s3_bucket_id.abi_encode()) } - IS3RegistryCalls::createS3BucketWithStorage( - IS3Registry::createS3BucketWithStorageCall { - name, - maxCapacity, - duration, - maxPayment, - }, - ) => { - env.charge(::WeightInfo::create_s3_bucket_with_storage())?; - let s3_bucket_id = pallet_s3_registry::NextS3BucketId::::get(); - pallet_s3_registry::Pallet::::create_s3_bucket_with_storage( - frame_origin, - name.as_bytes().to_vec(), - *maxCapacity, - BlockNumberFor::::from(*duration), - BalanceOf::::from(*maxPayment), - ) - .map_err(|e| revert(&e, "createS3BucketWithStorage failed"))?; - Ok(s3_bucket_id.abi_encode()) - } - IS3RegistryCalls::deleteS3Bucket(IS3Registry::deleteS3BucketCall { s3BucketId }) => { env.charge( ::WeightInfo::delete_s3_bucket(), diff --git a/precompiles/storage-provider-precompile/src/IWeb3Storage.sol b/precompiles/storage-provider-precompile/src/IWeb3Storage.sol index a6c8334f..9989eb7e 100644 --- a/precompiles/storage-provider-precompile/src/IWeb3Storage.sol +++ b/precompiles/storage-provider-precompile/src/IWeb3Storage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; +pragma solidity ^0.8.34; /// @title IWeb3Storage /// @notice Solidity interface for the web3-storage `pallet_storage_provider` @@ -10,19 +10,46 @@ pragma solidity ^0.8.0; /// /// Role tags: 0 = Admin, 1 = Writer, 2 = Reader. interface IWeb3Storage { - // --- Bucket lifecycle --------------------------------------------------- + // TODO: Find out way to make it re-useable + struct PrimitiveReplicaTerms { + /// Balance reserved by the owner to fund per-sync confirmations. + uint128 syncBalance; + /// Minimum blocks between sync confirmations the provider commits to. + uint32 minSyncInterval; + } + + struct PrimitiveAgreementTerms { + /// Owner bound by these terms (must be the caller's substrate-mapped + /// account at redemption). + bytes32 owner; + /// Storage quota committed by the provider, in bytes. + uint64 maxBytes; + /// Agreement duration in blocks from activation. + uint32 duration; + /// Price per byte per block locked at quote time. + uint128 pricePerByte; + /// Block number after which the quote is no longer redeemable. + uint32 validUntil; + /// Provider-chosen replay-protection nonce. + uint64 nonce; + /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). + bool hasReplicaParams; + /// Replica funding parameters; only read when `hasReplicaParams` is true. + PrimitiveReplicaTerms replicaParams; + } - /// Create an empty bucket. The caller (substrate-mapped) becomes the bucket - /// admin. Returns the new bucket id. - function createBucket(uint32 minProviders) external returns (uint64 bucketId); + // --- Bucket lifecycle --------------------------------------------------- - /// Create a bucket and atomically open a primary agreement against an - /// auto-matched provider. The caller's reserved balance must cover the - /// payment derived from `maxBytes * duration * matched-price`. - function createBucketWithStorage( - uint64 maxBytes, - uint32 duration, - uint128 maxPricePerByte + /// Redeem provider-signed agreement terms: create a bucket and open a + /// primary agreement atomically. `terms` must match the SCALE payload the + /// provider signed; `terms.owner` must be the caller's substrate-mapped + /// account. `signature` is the SCALE-encoded `MultiSignature` from the + /// provider's `/negotiate` response (variant byte + raw signature bytes). + /// Returns the new bucket id. + function establishStorageAgreement( + bytes32 provider, + PrimitiveAgreementTerms calldata terms, + bytes calldata signature ) external returns (uint64 bucketId); /// Freeze a bucket — append-only, irreversible. @@ -38,16 +65,6 @@ interface IWeb3Storage { // --- Agreement lifecycle ------------------------------------------------ - /// Request a primary storage agreement from `provider`. Provider must - /// `accept` separately (substrate side); the caller is bucket admin. - function requestPrimaryAgreement( - uint64 bucketId, - bytes32 provider, - uint64 maxBytes, - uint32 duration, - uint128 maxPayment - ) external; - /// Add funds and capacity to an existing agreement. function topUpAgreement( uint64 bucketId, diff --git a/precompiles/storage-provider-precompile/src/lib.rs b/precompiles/storage-provider-precompile/src/lib.rs index 9541a0bf..b513ad0b 100644 --- a/precompiles/storage-provider-precompile/src/lib.rs +++ b/precompiles/storage-provider-precompile/src/lib.rs @@ -51,6 +51,33 @@ where }) } +/// Rebuild the pallet's [`AgreementTermsOf`](pallet_storage_provider::AgreementTermsOf) +/// from its Solidity mirror so the SCALE encoding matches the payload the +/// provider signed. +fn decode_terms( + terms: &IWeb3Storage::PrimitiveAgreementTerms, +) -> Result, Error> +where + T: pallet_storage_provider::Config, + BalanceOf: From, + BlockNumberFor: From, +{ + Ok(storage_primitives::AgreementTerms { + owner: decode_account::(&terms.owner.0)?, + max_bytes: terms.maxBytes, + duration: BlockNumberFor::::from(terms.duration), + price_per_byte: BalanceOf::::from(terms.pricePerByte), + valid_until: BlockNumberFor::::from(terms.validUntil), + nonce: terms.nonce, + replica_params: terms + .hasReplicaParams + .then(|| storage_primitives::ReplicaTerms { + sync_balance: BalanceOf::::from(terms.replicaParams.syncBalance), + min_sync_interval: BlockNumberFor::::from(terms.replicaParams.minSyncInterval), + }), + }) +} + /// Decode a Solidity `uint8` as a `Role` enum (0 = Admin, 1 = Writer, 2 = Reader). fn decode_role(tag: u8) -> Result { match tag { @@ -100,38 +127,34 @@ where }; match input { - IWeb3StorageCalls::createBucket(IWeb3Storage::createBucketCall { minProviders }) => { - env.charge( - ::WeightInfo::create_bucket(), - )?; + IWeb3StorageCalls::establishStorageAgreement( + IWeb3Storage::establishStorageAgreementCall { + provider, + terms, + signature, + }, + ) => { + env.charge(::WeightInfo::establish_storage_agreement())?; + let provider = decode_account::(&provider.0)?; + let terms = decode_terms::(terms)?; + let sig = + sp_runtime::MultiSignature::decode(&mut signature.as_ref()).map_err(|e| { + revert( + &e, + "Invalid signature encoding: expected SCALE-encoded MultiSignature", + ) + })?; // `NextBucketId` is incremented inside the extrinsic; capture // the pre-dispatch value so we can return the id assigned to // this call. let bucket_id: BucketId = pallet_storage_provider::NextBucketId::::get(); - pallet_storage_provider::Pallet::::create_bucket( - frame_origin, - *minProviders, - ) - .map_err(|e| revert(&e, "createBucket failed"))?; - Ok(bucket_id.abi_encode()) - } - - IWeb3StorageCalls::createBucketWithStorage( - IWeb3Storage::createBucketWithStorageCall { - maxBytes, - duration, - maxPricePerByte, - }, - ) => { - env.charge(::WeightInfo::create_bucket_with_storage())?; - let bucket_id: BucketId = pallet_storage_provider::NextBucketId::::get(); - pallet_storage_provider::Pallet::::create_bucket_with_storage( + pallet_storage_provider::Pallet::::establish_storage_agreement( frame_origin, - *maxBytes, - BlockNumberFor::::from(*duration), - BalanceOf::::from(*maxPricePerByte), + provider, + terms, + sig, ) - .map_err(|e| revert(&e, "createBucketWithStorage failed"))?; + .map_err(|e| revert(&e, "establishStorageAgreement failed"))?; Ok(bucket_id.abi_encode()) } @@ -182,29 +205,6 @@ where Ok(Vec::new()) } - IWeb3StorageCalls::requestPrimaryAgreement( - IWeb3Storage::requestPrimaryAgreementCall { - bucketId, - provider, - maxBytes, - duration, - maxPayment, - }, - ) => { - env.charge(::WeightInfo::request_primary_agreement())?; - let provider = decode_account::(&provider.0)?; - pallet_storage_provider::Pallet::::request_primary_agreement( - frame_origin, - *bucketId, - provider, - *maxBytes, - BlockNumberFor::::from(*duration), - BalanceOf::::from(*maxPayment), - ) - .map_err(|e| revert(&e, "requestPrimaryAgreement failed"))?; - Ok(Vec::new()) - } - IWeb3StorageCalls::topUpAgreement(IWeb3Storage::topUpAgreementCall { bucketId, provider, From a1190809425a184df12bf5a608cd2ddbd997d333 Mon Sep 17 00:00:00 2001 From: Tung Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:43:28 +0700 Subject: [PATCH 26/44] Rewire user-interface following new agreement flow changes (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: updating conosle-ui * chore: double check runtime side * fix(console-ui): send MultiSignature payload as 0x-hex for PAPI v2 isCompat `buildSignedTermsArgs` was passing the 64-byte sig payload to `Enum()` as a raw `Uint8Array`. PAPI v2's `isCompatible` rejects raw bytes for fixed-length binary fields (`SizedHex`) — its check is `typeof value === "string" && value.startsWith("0x")` — and throws `Incompatible runtime entry Tx(S3Registry.create_s3_bucket)` before the tx is encoded. Variable-length binary (`Vec`) still wants a `Uint8Array`; only fixed-length wants the hex string. Encode the sig payload as a `0x`-prefixed hex string so it matches the descriptor type cli 0.21.x generates for MultiSignature variants. * fix(ui): set allowBuilds.esbuild=true for pnpm 11 install in subprocesses pnpm 11.1.2 treats a placeholder/missing build-script approval as a hard `[ERR_PNPM_IGNORED_BUILDS]` error when run from a non-TTY subprocess (vite's `runDepsStatusCheck` spawns `pnpm install` this way). The checked-in placeholder `esbuild: set this to true or false` is a literal string, not a boolean, so pnpm 11 errored out and vite refused to start. Replace the placeholder + `ignoredBuiltDependencies` entry with the actual `allowBuilds: { esbuild: true }` — esbuild's postinstall just links the platform binary, which we want to run anyway. * test(console-ui): drive e2e bucket creation through the real UI flow - Add createBucketViaUi / createBucketInFreshContext helpers that fill the form, click "Choose Provider & Create", then pick the first provider in the picker — the same path a real user walks. - Use the UI flow from bucket-create, encryption, members, and s3-objects specs instead of the chain-side createBucketViaApi shortcut. - Add provider-picker / provider-picker-select testids on ProviderPickerDialog so the picker is addressable. - Rewrite createBucketViaApi + createDriveViaApi in test-helpers to do HTTP /negotiate + atomic create_s3_bucket / create_drive, matching the new on-chain shape (provider, terms, sig). * feat(drive-ui): rewire create-drive for the negotiate → atomic establish flow drive-client: - Add negotiateTerms / buildSignedTermsArgs / listAvailableProviders. MultiSignature inner is hex string (SizedBytes(64) is Codec in PAPI v2). - Replace createDrive(options) with submitCreateDrive(name, provider, providerUrl, signed) — only the chain step, takes pre-negotiated terms so a failed submit can retry without re-negotiating. - Drop the obsolete waitForProvider poll (atomic flow → primary_providers is populated synchronously) and the `payment` field from DriveInfo. state hook: - createDrive(input) orchestrates negotiate → submit explicitly. Stash retry context per creation so retryCreation(id) re-fires just the chain step. - Narrow CreationStage to submitting | ready | failed (drop the now-dead created / waiting stages). - Expose listAvailableProviders for the picker. NewDriveDialog + ProviderPickerPanel: - Embed the provider picker inline in the create dialog (no separate modal). Picking a provider IS the submit; drop the "Choose Provider & Create" button. Form drops payment / minProviders, adds pricePerByte. - Status card adds a Retry on-chain submit button for failures after a successful negotiate. "Unlimited" rendered when maxCapacity == 0n. E2E: - New helpers createDriveViaUi(page, name) and createDriveInFreshContext( browser, name) that drive the form + embedded picker. - members / persistence / file-ops / realtime specs replace createDriveViaApi setup with the UI-driven helpers. Drop stale payment / minProviders / commitStrategy props. - drive-create.spec.ts walks the embedded picker (no submit button click). * chore(provider-ui): disable Agreements page pending flow rework Comment out the /agreements route, nav entry, and matching e2e test while the agreement request flow is being reworked. Also switch expected block time to Aura.SlotDuration. --------- Co-authored-by: Ilia Churin --- Cargo.lock | 19 + client/src/agreement.rs | 27 +- examples/papi/api.js | 4 +- examples/papi/s3-lifecycle.js | 4 +- runtime/Cargo.toml | 2 + runtime/src/lib.rs | 1 + runtimes/web3-storage-paseo/Cargo.toml | 2 + runtimes/web3-storage-paseo/src/lib.rs | 1 + .../e2e/helpers/createBucketViaUi.ts | 56 ++ .../e2e/integration/bucket-create.spec.ts | 24 +- .../e2e/integration/encryption.spec.ts | 11 +- .../e2e/integration/members.spec.ts | 12 +- .../e2e/integration/s3-objects.spec.ts | 16 +- .../src/components/CreationStatusCard.tsx | 128 +---- .../src/components/ProviderPickerDialog.tsx | 9 +- .../console-ui/src/components/S3Tab.tsx | 171 +++--- .../console-ui/src/hooks/useStorage.tsx | 58 +- user-interfaces/console-ui/src/lib/storage.ts | 297 ++++++---- .../console-ui/src/pages/Storage.tsx | 34 +- .../drive-ui/e2e/helpers/createDriveViaUi.ts | 77 +++ .../e2e/integration/drive-create.spec.ts | 12 +- .../drive-ui/e2e/integration/file-ops.spec.ts | 21 +- .../drive-ui/e2e/integration/members.spec.ts | 13 +- .../e2e/integration/persistence.spec.ts | 42 +- .../drive-ui/e2e/integration/realtime.spec.ts | 32 +- .../src/components/NewDriveDialog.tsx | 246 ++++---- .../src/components/ProviderPickerPanel.tsx | 177 ++++++ .../drive-ui/src/lib/drive-client.ts | 280 +++++++--- .../drive-ui/src/state/drive.state.ts | 135 ++++- user-interfaces/drive-ui/src/state/index.ts | 10 +- user-interfaces/pnpm-lock.yaml | 524 +++++++++++++----- user-interfaces/pnpm-workspace.yaml | 16 +- .../provider/e2e/integration/displays.spec.ts | 13 +- user-interfaces/provider/src/App.tsx | 4 +- .../provider/src/components/Header.tsx | 2 +- .../provider/src/lib/chain-client.ts | 38 +- .../papi/.papi/descriptors/package.json | 2 +- .../papi/.papi/metadata/parachain.scale | Bin 187025 -> 186426 bytes .../shared/papi/.papi/polkadot-api.json | 4 +- user-interfaces/shared/papi/package.json | 2 +- .../shared/test-helpers/src/buckets.ts | 181 ++++-- 41 files changed, 1860 insertions(+), 847 deletions(-) create mode 100644 user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts create mode 100644 user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts create mode 100644 user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx diff --git a/Cargo.lock b/Cargo.lock index 6457cb6c..60219a47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3386,6 +3386,23 @@ dependencies = [ "serde", ] +[[package]] +name = "frame-metadata-hash-extension" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9522fcf64f45004d010a9eb7c21f9854706cfc545856f33f932cd57295bbcf" +dependencies = [ + "array-bytes 6.2.3", + "const-hex", + "docify", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-runtime", +] + [[package]] name = "frame-support" version = "46.0.0" @@ -9566,6 +9583,7 @@ dependencies = [ "cumulus-primitives-utility", "frame-benchmarking", "frame-executive", + "frame-metadata-hash-extension", "frame-support", "frame-system", "frame-system-benchmarking", @@ -9634,6 +9652,7 @@ dependencies = [ "cumulus-primitives-utility", "frame-benchmarking", "frame-executive", + "frame-metadata-hash-extension", "frame-support", "frame-system", "frame-system-benchmarking", diff --git a/client/src/agreement.rs b/client/src/agreement.rs index 1c3eda6b..4de114c7 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -15,7 +15,7 @@ //! encoding has to be used on both sides — `sign_terms` enforces that. use codec::Encode; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Deserializer}; use sp_core::hashing::blake2_256; use sp_runtime::{AccountId32, MultiSignature}; use storage_primitives::AgreementTerms; @@ -39,10 +39,14 @@ pub struct NegotiateRequest { /// Account that will own the resulting bucket. pub owner: AccountId32, /// Storage quota requested, in bytes. + /// FIX: Safely handles the JS BigInt sent as a string + #[serde(deserialize_with = "deserialize_number_from_string_or_number")] pub max_bytes: u64, /// Agreement duration in blocks from activation. pub duration: u32, /// Price per byte per block the owner is willing to lock in. + /// FIX: Safely handles the JS BigInt sent as a string + #[serde(deserialize_with = "deserialize_number_from_string_or_number")] pub price_per_byte: u128, /// `Some(_)` to negotiate a replica agreement (per-sync funding + /// minimum sync interval); `None` for a primary agreement. @@ -92,3 +96,24 @@ mod hex_multi_signature { MultiSignature::decode(&mut &bytes[..]).map_err(serde::de::Error::custom) } } + + +// Universal helper function to accept either a JSON string or raw JSON number +fn deserialize_number_from_string_or_number<'de, T, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: std::str::FromStr + Deserialize<'de>, + ::Err: std::fmt::Display, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNumber { + String(String), + Number(T), + } + + match StringOrNumber::deserialize(deserializer)? { + StringOrNumber::String(s) => s.parse::().map_err(serde::de::Error::custom), + StringOrNumber::Number(n) => Ok(n), + } +} \ No newline at end of file diff --git a/examples/papi/api.js b/examples/papi/api.js index 9eb2d01a..2d6c43cb 100644 --- a/examples/papi/api.js +++ b/examples/papi/api.js @@ -42,7 +42,9 @@ export async function negotiateTerms(providerUrl, request) { const res = await fetch(`${providerUrl}/negotiate`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(request), + body: JSON.stringify(request, (_k, v) => + typeof v === "bigint" ? v.toString() : v, + ), }); if (!res.ok) { throw new Error( diff --git a/examples/papi/s3-lifecycle.js b/examples/papi/s3-lifecycle.js index e3a67116..99664283 100644 --- a/examples/papi/s3-lifecycle.js +++ b/examples/papi/s3-lifecycle.js @@ -94,13 +94,13 @@ async function main() { await ensureProviderRegistered(api, provider, PROVIDER_URL); console.log("\n=== Step 2: Negotiate signed agreement terms ==="); - const maxBytes = 1_048_576; // 1 MiB + const maxBytes = 1_048_576n; // 1 MiB const duration = 100; const signed = await negotiateTerms(PROVIDER_URL, { owner: client.address, max_bytes: maxBytes, duration, - price_per_byte: 0, + price_per_byte: 1n, replica_params: null, }); console.log( diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 813100c6..a87fc044 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -26,6 +26,7 @@ serde_json = { features = ["alloc"], workspace = true } # Substrate frame-benchmarking = { workspace = true, optional = true } frame-executive = { workspace = true } +frame-metadata-hash-extension = { workspace = true } frame-support = { workspace = true, features = ["experimental"] } frame-system = { workspace = true } frame-system-benchmarking = { workspace = true, optional = true } @@ -99,6 +100,7 @@ std = [ # Substrate "frame-benchmarking?/std", "frame-executive/std", + "frame-metadata-hash-extension/std", "frame-support/std", "frame-system-benchmarking?/std", "frame-system-rpc-runtime-api/std", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index d563330d..3574d6bd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -138,6 +138,7 @@ pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< frame_system::CheckNonce, frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, + frame_metadata_hash_extension::CheckMetadataHash, pallet_revive::evm::tx_extension::SetOrigin, ), >; diff --git a/runtimes/web3-storage-paseo/Cargo.toml b/runtimes/web3-storage-paseo/Cargo.toml index 69abb6f6..13873cb3 100644 --- a/runtimes/web3-storage-paseo/Cargo.toml +++ b/runtimes/web3-storage-paseo/Cargo.toml @@ -26,6 +26,7 @@ serde_json = { features = ["alloc"], workspace = true } # Substrate frame-benchmarking = { workspace = true, optional = true } frame-executive = { workspace = true } +frame-metadata-hash-extension = { workspace = true } frame-support = { workspace = true, features = ["experimental"] } frame-system = { workspace = true } frame-system-benchmarking = { workspace = true, optional = true } @@ -104,6 +105,7 @@ std = [ # Substrate "frame-benchmarking?/std", "frame-executive/std", + "frame-metadata-hash-extension/std", "frame-support/std", "frame-system-benchmarking?/std", "frame-system-rpc-runtime-api/std", diff --git a/runtimes/web3-storage-paseo/src/lib.rs b/runtimes/web3-storage-paseo/src/lib.rs index 09a0c43c..a0abcb4a 100644 --- a/runtimes/web3-storage-paseo/src/lib.rs +++ b/runtimes/web3-storage-paseo/src/lib.rs @@ -140,6 +140,7 @@ pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< frame_system::CheckNonce, frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, + frame_metadata_hash_extension::CheckMetadataHash, pallet_revive::evm::tx_extension::SetOrigin, ), >; diff --git a/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts b/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts new file mode 100644 index 00000000..e6aad842 --- /dev/null +++ b/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts @@ -0,0 +1,56 @@ +import { expect, type Browser, type Page } from "@playwright/test"; + +/** + * Drive the console-ui through the real user create-bucket flow: + * + * 1. Open the Storage tab and the create form (auto-opens when there are + * no buckets; otherwise click "New Bucket"). + * 2. Fill the bucket name (capacity / duration / price defaults stay). + * 3. Click "Choose Provider & Create" — opens the picker dialog. + * 4. Click the first available provider's Select button. The UI runs the + * HTTP `POST /negotiate` step and then submits `create_s3_bucket` + * atomically. + * 5. Wait for the new bucket to appear in the selector. + * + */ +export async function createBucketViaUi(page: Page, name: string): Promise { + await page.getByTestId("nav-storage").click(); + if (!(await page.getByTestId("s3-create-bucket-form").isVisible().catch(() => false))) { + await page.getByTestId("s3-new-bucket").click(); + } + await expect(page.getByTestId("s3-create-bucket-form")).toBeVisible(); + + await page.getByTestId("s3-bucket-name-input").fill(name); + await page.getByTestId("s3-create-submit").click(); + + await expect(page.getByTestId("provider-picker")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("provider-picker-select").first().click(); + + await expect(page.getByTestId("s3-bucket-selector")).toContainText(name, { + timeout: 90_000, + }); +} + +/** + * `beforeAll` variant — opens a fresh browser context (with the same + * localStorage seed as the `localPage` fixture), navigates to the app, + * drives the UI create flow, and closes the context. Use from `test.beforeAll` + * when you need a bucket created via the UI before any individual test runs. + */ +export async function createBucketInFreshContext( + browser: Browser, + name: string, +): Promise { + const context = await browser.newContext(); + await context.addInitScript(() => { + localStorage.setItem("web3-storage-selected-network", "local"); + }); + const page = await context.newPage(); + try { + await page.goto("/"); + await expect(page.getByTestId("block-number")).toBeVisible({ timeout: 30_000 }); + await createBucketViaUi(page, name); + } finally { + await context.close(); + } +} diff --git a/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts b/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts index 22bb5c78..3de29ba7 100644 --- a/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts @@ -1,8 +1,13 @@ /** * Bucket creation spec (slow ~30s). * - * Walks the Storage page UI to create a bucket, then verifies the bucket - * appears in the selector AND on-chain at S3Registry.S3Buckets. + * Walks the Storage page UI through the two-step create flow: + * 1. Fill the form and click "Choose Provider & Create" — opens picker + * 2. Pick the first available provider — UI negotiates over HTTP and + * submits `create_s3_bucket` atomically + * + * Then verifies the bucket appears in the selector AND on-chain at + * `S3Registry.S3Buckets`. */ import { test, expect } from "../fixtures"; import { @@ -32,11 +37,20 @@ test("create bucket via UI → on-chain S3Buckets matches", async ({ localPage } const name = `bucket-create-${Date.now()}`; await localPage.getByTestId("s3-bucket-name-input").fill(name); - // Use defaults for capacity / duration / max-payment (set in the component). + // Defaults for capacity / duration / price-per-byte are set in the component. await localPage.getByTestId("s3-create-submit").click(); - // Wait for the new bucket to appear in the selector. After creation the UI - // auto-selects it. + // Step 1 of the new flow: the picker opens with the list of available + // providers. Globalsetup registered Alice as the only provider, so we + // can confidently click the first Select button. + await expect(localPage.getByTestId("provider-picker")).toBeVisible({ + timeout: 30_000, + }); + await localPage.getByTestId("provider-picker-select").first().click(); + + // Step 2 runs automatically (HTTP negotiate → on-chain submit). Wait for + // the new bucket to appear in the selector — the UI auto-selects it on + // success. await expect(localPage.getByTestId("s3-bucket-selector")).toContainText(name, { timeout: 90_000, }); diff --git a/user-interfaces/console-ui/e2e/integration/encryption.spec.ts b/user-interfaces/console-ui/e2e/integration/encryption.spec.ts index b4a7c3eb..7335e6e6 100644 --- a/user-interfaces/console-ui/e2e/integration/encryption.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/encryption.spec.ts @@ -5,21 +5,18 @@ * Generate populates a valid 64-hex-char key. */ import { test, expect } from "../fixtures"; -import { - Alice, - cleanupBuckets, - createBucketViaApi, -} from "@web3-storage/test-helpers"; +import { Alice, cleanupBuckets } from "@web3-storage/test-helpers"; +import { createBucketInFreshContext } from "../helpers/createBucketViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(120_000); let bucketName: string; -test.beforeAll(async () => { +test.beforeAll(async ({ browser }) => { test.setTimeout(120_000); bucketName = `enc-${Date.now()}`; - await createBucketViaApi(Alice, { name: bucketName }); + await createBucketInFreshContext(browser, bucketName); }); test.afterAll(async () => { diff --git a/user-interfaces/console-ui/e2e/integration/members.spec.ts b/user-interfaces/console-ui/e2e/integration/members.spec.ts index 7dce333d..db6aef5b 100644 --- a/user-interfaces/console-ui/e2e/integration/members.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/members.spec.ts @@ -6,22 +6,18 @@ * SS58/duplicate tests assert on toast text. */ import { test, expect } from "../fixtures"; -import { - Alice, - Bob, - cleanupBuckets, - createBucketViaApi, -} from "@web3-storage/test-helpers"; +import { Alice, Bob, cleanupBuckets } from "@web3-storage/test-helpers"; +import { createBucketInFreshContext } from "../helpers/createBucketViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); let bucketName: string; -test.beforeAll(async () => { +test.beforeAll(async ({ browser }) => { test.setTimeout(120_000); bucketName = `members-${Date.now()}`; - await createBucketViaApi(Alice, { name: bucketName }); + await createBucketInFreshContext(browser, bucketName); }); test.afterAll(async () => { diff --git a/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts b/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts index 7baef55a..747a7b15 100644 --- a/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts @@ -1,25 +1,23 @@ /** * S3 object operations spec. * - * One bucket created via api in beforeAll (faster than walking the UI flow); - * tests upload / download bytes / delete reuse it. + * One bucket created via the actual UI flow in beforeAll + * (negotiate-with-provider → create_s3_bucket); tests upload / download + * bytes / delete reuse it. */ import { test, expect } from "../fixtures"; -import { - Alice, - cleanupBuckets, - createBucketViaApi, -} from "@web3-storage/test-helpers"; +import { Alice, cleanupBuckets } from "@web3-storage/test-helpers"; +import { createBucketInFreshContext } from "../helpers/createBucketViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); let bucketName: string; -test.beforeAll(async () => { +test.beforeAll(async ({ browser }) => { test.setTimeout(120_000); bucketName = `objects-${Date.now()}`; - await createBucketViaApi(Alice, { name: bucketName }); + await createBucketInFreshContext(browser, bucketName); }); test.afterAll(async () => { diff --git a/user-interfaces/console-ui/src/components/CreationStatusCard.tsx b/user-interfaces/console-ui/src/components/CreationStatusCard.tsx index b6f7dc01..677566c5 100644 --- a/user-interfaces/console-ui/src/components/CreationStatusCard.tsx +++ b/user-interfaces/console-ui/src/components/CreationStatusCard.tsx @@ -1,64 +1,39 @@ -import { useState, useEffect, useRef } from "react"; -import { CheckCircle2, RefreshCw, XCircle, X, Clock, AlertTriangle } from "lucide-react"; +import { CheckCircle2, RefreshCw, XCircle, X } from "lucide-react"; import { Button } from "@/components/ui/button"; -export type CreationStage = - | "submitting" - | "created" - | "waiting" - | "ready" - | "failed"; +/** + * Stages the new create-bucket flow can be in. Bucket creation is now a + * single atomic chain tx (negotiate happens over HTTP first, then one + * `create_s3_bucket` extrinsic), so the only transient state is + * `submitting`; from there it terminates as `ready` or `failed`. + */ +export type CreationStage = "submitting" | "ready" | "failed"; export interface CreationStatusItem { id: string; name: string; type: "bucket"; stage: CreationStage; - /** Status message from the provider polling loop */ - statusMessage?: string; - /** Elapsed milliseconds since the waiting stage began */ + /** Elapsed milliseconds (unused now but kept so callers can keep tracking). */ elapsedMs: number; error?: string; createdAt: number; - /** Layer0 bucket ID — needed for provider picker retry */ + /** Layer0 bucket ID populated once the tx lands. */ bucketId?: bigint; } interface CreationStatusCardProps { item: CreationStatusItem; onDismiss: (id: string) => void; - onChooseProvider?: (item: CreationStatusItem) => void; + /** + * Called when the user clicks Retry on a failed item. Only shown when + * a retry is meaningful — e.g. the negotiation succeeded but the on-chain + * submit failed, so the caller can re-fire the chain submit with the + * same signed terms. + */ + onRetry?: (id: string) => void; } -/** Returns a human-readable elapsed time string */ -function formatElapsed(ms: number): string { - const sec = Math.round(ms / 1000); - if (sec < 60) return `${sec}s`; - const min = Math.floor(sec / 60); - const rem = sec % 60; - return `${min}m ${rem}s`; -} - -type TimingTier = "normal" | "slow" | "very_slow"; - -function getTimingTier(elapsedMs: number): TimingTier { - if (elapsedMs >= 120_000) return "very_slow"; - if (elapsedMs >= 60_000) return "slow"; - return "normal"; -} - -const timingWarnings: Record = { - normal: null, - slow: "Taking longer than expected — the provider may be under load", - very_slow: "Something may be wrong — the provider could be offline or not processing agreements", -}; - -const timingColors: Record = { - normal: "text-muted-foreground", - slow: "text-amber-500", - very_slow: "text-red-500", -}; - interface StageDisplay { icon: "spin" | "check" | "error"; color: string; @@ -74,31 +49,15 @@ function getStageDisplay(item: CreationStatusItem): StageDisplay { icon: "spin", color: "text-amber-500", borderColor: "border-amber-500/30", - description: "Signing transaction and submitting to the blockchain...", + description: "Negotiating with provider and submitting to the blockchain...", badgeLabel: "Submitting", }; - case "created": - return { - icon: "check", - color: "text-blue-500", - borderColor: "border-blue-500/30", - description: "Registered on-chain. Requesting storage agreement from provider...", - badgeLabel: "On-chain", - }; - case "waiting": - return { - icon: "spin", - color: "text-blue-500", - borderColor: "border-blue-500/30", - description: item.statusMessage || "Waiting for provider to accept the storage agreement...", - badgeLabel: "Agreement pending", - }; case "ready": return { icon: "check", color: "text-green-500", borderColor: "border-green-500/30", - description: "Provider accepted the agreement — storage is ready to use", + description: "Bucket created and the storage agreement is live", badgeLabel: "Ready", }; case "failed": @@ -121,25 +80,9 @@ function StageIcon({ type, color }: { type: "spin" | "check" | "error"; color: s export default function CreationStatusCard({ item, onDismiss, - onChooseProvider, + onRetry, }: CreationStatusCardProps) { const display = getStageDisplay(item); - const tier = getTimingTier(item.elapsedMs); - const warning = (item.stage === "waiting" || item.stage === "created") ? timingWarnings[tier] : null; - - // Live elapsed timer — ticks every second while in active stages - const [, setTick] = useState(0); - const tickRef = useRef>(undefined); - useEffect(() => { - const active = item.stage === "submitting" || item.stage === "created" || item.stage === "waiting"; - if (active) { - tickRef.current = setInterval(() => setTick(t => t + 1), 1000); - return () => clearInterval(tickRef.current); - } - clearInterval(tickRef.current); - }, [item.stage]); - - const showElapsed = item.stage === "waiting" || item.stage === "created"; const isDismissible = item.stage === "ready" || item.stage === "failed"; const label = "S3 Bucket"; @@ -148,9 +91,7 @@ export default function CreationStatusCard({ ? "rgb(34 197 94 / 0.1)" : item.stage === "failed" ? "rgb(239 68 68 / 0.1)" - : item.stage === "submitting" - ? "rgb(245 158 11 / 0.1)" - : "rgb(59 130 246 / 0.1)"; + : "rgb(245 158 11 / 0.1)"; return (
@@ -159,7 +100,6 @@ export default function CreationStatusCard({
- {/* Header: name + badge */}

{label}: {item.name} @@ -170,42 +110,26 @@ export default function CreationStatusCard({ > {display.badgeLabel} - {showElapsed && ( - - - {formatElapsed(item.elapsedMs)} - - )}

- {/* Status description */}

{display.description}

- {/* Timing warning — shows alongside status when things are slow */} - {warning && ( -
- - {warning} -
- )} - - {/* Action buttons for failed stage */} - {item.stage === "failed" && onChooseProvider && item.bucketId && ( + {item.stage === "failed" && onRetry && (
)}
- {/* Dismiss button — only for terminal states */} {isDismissible && ( diff --git a/user-interfaces/console-ui/src/components/S3Tab.tsx b/user-interfaces/console-ui/src/components/S3Tab.tsx index ccd6e2cf..23d8b685 100644 --- a/user-interfaces/console-ui/src/components/S3Tab.tsx +++ b/user-interfaces/console-ui/src/components/S3Tab.tsx @@ -25,7 +25,8 @@ import { import { useStorage } from "@/hooks/useStorage"; import { toast } from "@/components/ui/toaster"; import { formatBytes, truncateHash } from "@/lib/utils"; -import type { BucketInfo, S3ObjectInfo } from "@/lib/storage"; +import type { AvailableProvider, BucketInfo, S3ObjectInfo, SignedTerms } from "@/lib/storage"; +import { negotiateTerms, parseMultiaddrToHttp } from "@/lib/storage"; import { EncryptionKey, bytesToHex } from "@/lib/encryption"; import CreationStatusCard, { type CreationStatusItem } from "./CreationStatusCard"; import ProviderPickerDialog from "./ProviderPickerDialog"; @@ -41,15 +42,13 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { buckets, loading, refreshBuckets, - createBucket, + submitCreateBucket, deleteBucket, putObject, listObjects, deleteObject, downloadS3Object, signerAddress, - waitForProvider, - requestAgreementWithProvider, fetchBucketMembers, setEncryption, isEncrypted, @@ -61,19 +60,17 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { const [loadingObjects, setLoadingObjects] = useState(false); const fileInputRef = useRef(null); - // Create bucket dialog + // Create bucket dialog. Defaults: 10 MiB capacity, 10k blocks duration. const [showCreateBucket, setShowCreateBucket] = useState(false); const [newBucketName, setNewBucketName] = useState(""); - // Defaults: 10 MB capacity (10 × 2^20 bytes), 10k blocks duration - // maxPayment must cover: price_per_byte(1e6) × capacity × duration × 1.2 buffer const [bucketCapacity, setBucketCapacity] = useState("10485760"); const [bucketDuration, setBucketDuration] = useState("10000"); - const [bucketMaxPayment, setBucketMaxPayment] = useState("120000000000000000"); + const [bucketPricePerByte, setBucketPricePerByte] = useState("0"); const [creating, setCreating] = useState(false); // Creation status tracking const [creations, setCreations] = useState([]); - const [pickerTarget, setPickerTarget] = useState(null); + const [pickerOpen, setPickerOpen] = useState(false); // Role-based access const [userRole, setUserRole] = useState(null); @@ -191,80 +188,114 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { return true; }; - const waitAndTrack = async (creationId: string, layer0BucketId: bigint) => { - updateCreation(creationId, { stage: "waiting", elapsedMs: 0 }); - try { - await waitForProvider(layer0BucketId, (status, elapsedMs) => { - updateCreation(creationId, { statusMessage: status, elapsedMs }); - }); - updateCreation(creationId, { stage: "ready" }); - } catch (err) { - updateCreation(creationId, { - stage: "failed", - error: err instanceof Error ? err.message : "Provider did not accept. Try choosing a provider manually.", - }); - } - }; - const [createError, setCreateError] = useState(null); - const handleCreateBucket = async () => { + // Step 1: validate the form, then open the provider picker. + // (Bucket creation requires a provider-signed agreement up front — the + // chain no longer auto-discovers, and the bucket cannot exist without it.) + const handleCreateBucket = () => { if (!newBucketName.trim() || !validateBucketName(newBucketName)) { toast({ title: "Error", description: "Invalid bucket name (3-63 chars, lowercase, S3 rules)", variant: "destructive" }); return; } - - setCreating(true); setCreateError(null); + setPickerOpen(true); + }; + // Per-creation retry context. Lets a failed on-chain submit be retried + // with the same signed terms — no need to bother the provider again + // (and burn a nonce) just because the tx fee estimation hiccuped or the + // user was momentarily disconnected. Cleared once the bucket is ready. + interface RetryCtx { + name: string; + provider: AvailableProvider; + url: string; + signed: SignedTerms; + } + const retryCtxRef = useRef>(new Map()); + + // Submit `create_s3_bucket` with already-negotiated signed terms. Used + // both by the first attempt and by the retry button on a failed card. + const runChainSubmit = useCallback(async (itemId: string, ctx: RetryCtx) => { + updateCreation(itemId, { stage: "submitting", error: undefined }); try { - const bucket = await createBucket(newBucketName, { - capacity: BigInt(bucketCapacity), - duration: parseInt(bucketDuration, 10), - maxPayment: BigInt(bucketMaxPayment), - }); + const bucket = await submitCreateBucket(ctx.name, ctx.provider.account, ctx.url, ctx.signed); + updateCreation(itemId, { stage: "ready", bucketId: bucket.layer0BucketId }); + retryCtxRef.current.delete(itemId); setSelectedBucket(bucket); setShowCreateBucket(false); setNewBucketName(""); toast({ title: "Success", description: `Bucket "${bucket.name}" created` }); } catch (err) { - const msg = err instanceof Error ? err.message : "Failed to create bucket"; - // Show friendly error for common cases - if (msg.includes("NoProvidersAvailable")) { - setCreateError("No storage providers available. Make sure a provider is running and accepting agreements."); - } else { - setCreateError(msg); - } + const msg = err instanceof Error ? err.message : "Failed to submit on chain"; + updateCreation(itemId, { stage: "failed", error: msg }); } finally { setCreating(false); } - }; + }, [submitCreateBucket, updateCreation]); + + // Step 2 (orchestration): caller picked a provider. Negotiate signed + // terms over HTTP, then submit on chain. Negotiation and submission are + // tracked as one CreationStatusItem but stored in two separate stages + // so a chain failure can be retried without re-negotiating. + const handleProviderSelect = async (provider: AvailableProvider) => { + setPickerOpen(false); + const url = parseMultiaddrToHttp(provider.multiaddr); + if (!url) { + setCreateError(`Provider ${provider.account} has an unparseable multiaddr: ${provider.multiaddr}`); + return; + } - const handleProviderSelect = async (providerAccount: string) => { - const bucketId = pickerTarget?.bucketId; - if (!pickerTarget || !bucketId) return; - const itemId = pickerTarget.id; - setPickerTarget(null); + const itemId = `bucket-${Date.now()}`; + const name = newBucketName; + setCreations(prev => [...prev, { + id: itemId, + name, + type: "bucket", + stage: "submitting", + elapsedMs: 0, + createdAt: Date.now(), + }]); + setCreating(true); - updateCreation(itemId, { stage: "submitting", error: undefined }); + // Phase A: negotiate. Failure here means re-negotiate from scratch on retry. + let signed: SignedTerms; try { - await requestAgreementWithProvider( - bucketId, - providerAccount, - BigInt(bucketCapacity), - parseInt(bucketDuration, 10), - BigInt(bucketMaxPayment), - ); - updateCreation(itemId, { stage: "created" }); - waitAndTrack(itemId, bucketId); - } catch (err) { - updateCreation(itemId, { - stage: "failed", - error: err instanceof Error ? err.message : "Agreement request failed", + signed = await negotiateTerms(url, { + owner: signerAddress!, + max_bytes: BigInt(bucketCapacity), + duration: parseInt(bucketDuration, 10), + price_per_byte: BigInt(bucketPricePerByte || "0"), + replica_params: null, }); + } catch (err) { + const msg = err instanceof Error ? err.message : "Failed to negotiate with provider"; + updateCreation(itemId, { stage: "failed", error: msg }); + setCreateError(msg); + setCreating(false); + return; } + + // Phase B: chain submit. Stash retry context first so a failure leaves + // a retry handle attached to the CreationStatusItem. + const ctx: RetryCtx = { name, provider, url, signed }; + retryCtxRef.current.set(itemId, ctx); + await runChainSubmit(itemId, ctx); }; + // Retry handler attached to failed CreationStatusItems. If we have the + // signed terms cached, re-fire just the chain submit; otherwise the user + // has to start over (covered by the picker). + const handleRetryCreation = useCallback((itemId: string) => { + const ctx = retryCtxRef.current.get(itemId); + if (!ctx) { + toast({ title: "Cannot retry", description: "Negotiation context expired — start a new bucket creation.", variant: "destructive" }); + return; + } + setCreating(true); + void runChainSubmit(itemId, ctx); + }, [runChainSubmit]); + const handleGenerateKey = async () => { const { rawKey } = await EncryptionKey.generate(); const hex = bytesToHex(rawKey); @@ -453,8 +484,14 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { setBucketDuration(e.target.value)} />
- - setBucketMaxPayment(e.target.value)} /> + + setBucketPricePerByte(e.target.value)} + />
{createError && ( @@ -462,7 +499,7 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { )}
@@ -470,21 +507,21 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { )} - {/* Creation status cards */} + {/* Creation status cards (submitting → ready | failed) */} {creations.map(item => ( setPickerTarget(item)} + onRetry={retryCtxRef.current.has(item.id) ? handleRetryCreation : undefined} /> ))} - {/* Provider picker dialog */} - {pickerTarget && ( + {/* Provider picker dialog — opens up front, before any chain tx */} + {pickerOpen && ( setPickerTarget(null)} + onClose={() => setPickerOpen(false)} onSelect={handleProviderSelect} requiredCapacity={BigInt(bucketCapacity)} requiredDuration={parseInt(bucketDuration, 10)} diff --git a/user-interfaces/console-ui/src/hooks/useStorage.tsx b/user-interfaces/console-ui/src/hooks/useStorage.tsx index 62ecc079..40d80726 100644 --- a/user-interfaces/console-ui/src/hooks/useStorage.tsx +++ b/user-interfaces/console-ui/src/hooks/useStorage.tsx @@ -12,7 +12,7 @@ import { type BucketMember, type ProviderEndpointInfo, type AvailableProvider, - type CreateBucketOptions, + type SignedTerms, type UploadResult, type PutObjectOptions, type S3ObjectInfo, @@ -35,7 +35,14 @@ interface StorageState { setSigner: (seed: string) => Promise; // Buckets (S3) - createBucket: (name: string, options: CreateBucketOptions) => Promise; + // Step 2 of bucket creation. Step 1 (HTTP `negotiateTerms`) runs in the + // caller so a failed chain submit can be retried with the same `signed`. + submitCreateBucket: ( + name: string, + providerAccount: string, + providerUrl: string, + signed: SignedTerms, + ) => Promise; refreshBuckets: () => Promise; deleteBucket: (name: string) => Promise; putObject: (bucketName: string, key: string, data: Uint8Array, bucketId: bigint, options?: PutObjectOptions) => Promise; @@ -45,9 +52,7 @@ interface StorageState { // Provider checkProviderHealth: (bucketId: bigint) => Promise; - waitForProvider: (bucketId: bigint, onProgress?: (status: string, elapsedMs: number, attempt: number) => void) => Promise; listAvailableProviders: () => Promise; - requestAgreementWithProvider: (bucketId: bigint, providerAccount: string, maxBytes: bigint, duration: number, maxPayment: bigint) => Promise; // Bucket Members & Permissions fetchBucketMembers: (bucketId: bigint) => Promise; @@ -187,22 +192,27 @@ export function StorageProvider({ children }: { children: ReactNode }) { // --- Bucket Operations --- - const createBucket = useCallback(async (name: string, options: CreateBucketOptions): Promise => { + const submitCreateBucket = useCallback(async ( + name: string, + providerAccount: string, + providerUrl: string, + signed: SignedTerms, + ): Promise => { if (!client) throw new Error("Client not connected"); if (!signerAddress) throw new Error("Signer not set"); setLoading(true); setError(null); try { - console.log("[useStorage] createBucket:", name, options); - const bucket = await client.createBucket(name, options); - console.log("[useStorage] createBucket success:", bucket); + console.log("[useStorage] submitCreateBucket:", name, providerAccount, providerUrl); + const bucket = await client.submitCreateBucket(name, providerAccount, providerUrl, signed); + console.log("[useStorage] submitCreateBucket success:", bucket); // Skip refreshBuckets — caller already has BucketInfo from events. // Add to local state directly for immediate UI update. setBuckets((prev) => [...prev, bucket]); return bucket; } catch (err) { - console.error("[useStorage] createBucket failed:", err); + console.error("[useStorage] submitCreateBucket failed:", err); const msg = err instanceof Error ? err.message : String(err); console.error("[useStorage] Error message:", msg); if (msg.includes("incompatible") || msg.includes("runtime")) { @@ -293,37 +303,11 @@ export function StorageProvider({ children }: { children: ReactNode }) { return client.checkProviderHealth(bucketId); }, [client]); - const waitForProvider = useCallback(async ( - bucketId: bigint, - onProgress?: (status: string, elapsedMs: number, attempt: number) => void, - ): Promise => { - if (!client) throw new Error("Client not connected"); - return client.waitForProvider(bucketId, onProgress); - }, [client]); - const listAvailableProviders = useCallback(async (): Promise => { if (!client) throw new Error("Client not connected"); return client.listAvailableProviders(); }, [client]); - const requestAgreementWithProvider = useCallback(async ( - bucketId: bigint, - providerAccount: string, - maxBytes: bigint, - duration: number, - maxPayment: bigint, - ): Promise => { - if (!client) throw new Error("Client not connected"); - setError(null); - try { - await client.requestAgreementWithProvider(bucketId, providerAccount, maxBytes, duration, maxPayment); - } catch (err) { - const msg = err instanceof Error ? err.message : "Failed to request agreement"; - setError(msg); - throw err; - } - }, [client]); - // --- Bucket Members & Permissions --- const fetchBucketMembers = useCallback(async (bucketId: bigint): Promise => { @@ -458,7 +442,7 @@ export function StorageProvider({ children }: { children: ReactNode }) { balance, providerCapacity, setSigner, - createBucket, + submitCreateBucket, refreshBuckets, deleteBucket, putObject, @@ -466,9 +450,7 @@ export function StorageProvider({ children }: { children: ReactNode }) { listObjects, deleteObject, checkProviderHealth, - waitForProvider, listAvailableProviders, - requestAgreementWithProvider, fetchBucketMembers, addBucketMember, removeBucketMember, diff --git a/user-interfaces/console-ui/src/lib/storage.ts b/user-interfaces/console-ui/src/lib/storage.ts index a4b3ac5b..1767ac33 100644 --- a/user-interfaces/console-ui/src/lib/storage.ts +++ b/user-interfaces/console-ui/src/lib/storage.ts @@ -33,10 +33,132 @@ export interface UploadResult { size: number; } -export interface CreateBucketOptions { - capacity: bigint; +/** + * Provider-signed agreement terms returned by `POST /negotiate` on the + * provider node. The signature is the SCALE-encoded `MultiSignature` as + * hex (e.g. `0x01<64-byte-sr25519-sig>`). + */ +export interface SignedTerms { + terms: { + owner: string; + max_bytes: number | bigint; + duration: number; + price_per_byte: number | bigint; + valid_until: number; + nonce: number | bigint; + replica_params: unknown | null; + }; + signature: string; +} + +export interface NegotiateRequest { + owner: string; + max_bytes: number | bigint; duration: number; - maxPayment: bigint; + price_per_byte: number | bigint; + replica_params: unknown | null; +} + +/** + * Parse a multiaddr (e.g. `/ip4/127.0.0.1/tcp/3333` or `/dns4/host/tcp/3333`) + * into an HTTP base URL. Exported so the UI can derive the negotiate + * endpoint from a picked provider's chain metadata before any agreement + * exists on chain. + */ +export function parseMultiaddrToHttp(multiaddr: string): string | null { + const parts = multiaddr.split("/").filter(Boolean); + let host: string | null = null; + let port: string | null = null; + for (let i = 0; i < parts.length; i++) { + if ( + (parts[i] === "ip4" || + parts[i] === "ip6" || + parts[i] === "dns4" || + parts[i] === "dns6") && + parts[i + 1] + ) { + host = parts[i + 1]; + } + if (parts[i] === "tcp" && parts[i + 1]) { + port = parts[i + 1]; + } + } + return host && port ? `http://${host}:${port}` : null; +} + +/** + * POST a `NegotiateRequest` to the provider's `/negotiate` endpoint and + * return the provider-signed terms bundle. Mirrors + * `examples/papi/api.js::negotiateTerms`. + */ +export async function negotiateTerms( + providerUrl: string, + request: NegotiateRequest, +): Promise { + const res = await fetch(`${providerUrl.replace(/\/$/, "")}/negotiate`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request, (_k, v) => + typeof v === "bigint" ? v.toString() : v, + ), + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`/negotiate failed: ${res.status} ${body}`); + } + return res.json(); +} + +// MultiSignature SCALE variant order from sp_runtime. +const MULTI_SIGNATURE_VARIANT: Record = { + 0: "Ed25519", + 1: "Sr25519", + 2: "Ecdsa", + 3: "Eth", +}; + +function hexToBytes(hex: string): Uint8Array { + const h = hex.startsWith("0x") ? hex.slice(2) : hex; + const out = new Uint8Array(h.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(h.substr(i * 2, 2), 16); + } + return out; +} + +/** + * Build the agreement terms and sign it + */ +export function buildSignedTermsArgs( + providerAccount: string, + signed: SignedTerms, +) { + const sigBytes = hexToBytes(signed.signature); + if (sigBytes.length < 1) { + throw new Error("signature too short to contain a MultiSignature variant byte"); + } + const variantName = MULTI_SIGNATURE_VARIANT[sigBytes[0]]; + if (!variantName) { + throw new Error(`unknown MultiSignature variant byte: ${sigBytes[0]}`); + } + const sigPayloadHex = + "0x" + + Array.from(sigBytes.slice(1)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + const sig = Enum(variantName as any, sigPayloadHex); + + const t = signed.terms; + const terms = { + owner: t.owner, + max_bytes: BigInt(t.max_bytes), + duration: t.duration, + price_per_byte: BigInt(t.price_per_byte), + valid_until: t.valid_until, + nonce: BigInt(t.nonce), + replica_params: (t.replica_params ?? undefined) as any, + }; + return { provider: providerAccount, terms, sig }; } export interface CheckpointConfig { @@ -295,6 +417,38 @@ export class StorageClient { // --- Provider Resolution --- + /** + * Resolve the HTTP endpoint for a bucket's primary provider by reading + * on-chain bucket data and provider multiaddr. + */ + private async resolveProviderEndpoint(bucketId: bigint): Promise { + if (!this.api) throw new Error("Not connected"); + + const bucket = await this.api.query.StorageProvider.Buckets.getValue(bucketId); + if (!bucket) throw new Error(`Bucket ${bucketId} not found on chain`); + + const providers: string[] = bucket.primary_providers ?? []; + if (providers.length === 0) { + throw new Error(`Bucket ${bucketId} has no primary providers`); + } + + // Try each provider until we find one with a valid multiaddr + for (const providerAccount of providers) { + const provider = await this.api.query.StorageProvider.Providers.getValue(providerAccount); + if (!provider) continue; + + // multiaddr is a BoundedVec — decode to string + const multiaddrStr = new TextDecoder().decode(provider.multiaddr); + + console.log(`[StorageClient] Provider ${providerAccount} multiaddr raw:`, provider.multiaddr, `decoded: "${multiaddrStr}"`); + const url = parseMultiaddrToHttp(multiaddrStr); + console.log(`[StorageClient] Parsed URL:`, url); + if (url) return url; + } + + throw new Error(`Could not resolve HTTP endpoint for bucket ${bucketId} providers`); + } + /** * Get the provider HTTP URL for a bucket, with caching. * Retries a few times if the bucket has no providers yet (agreement pending acceptance). @@ -328,70 +482,6 @@ export class StorageClient { } } - /** - * Wait for a bucket's provider to become available. - * Polls for up to ~150 seconds with increasing backoff. - * Calls onProgress with elapsed seconds so the UI can show timing warnings. - */ - async waitForProvider( - bucketId: bigint, - onProgress?: (status: string, elapsedMs: number, attempt: number) => void, - ): Promise { - this.invalidateProviderCache(bucketId); - - // Poll schedule: 20 attempts over ~150s - // Early: 3s intervals, then 6s, then 10s - const intervals = [ - 0, 3000, 3000, 3000, 3000, 3000, // 0-15s: every 3s - 6000, 6000, 6000, 6000, 6000, // 15-45s: every 6s - 10000, 10000, 10000, 10000, 10000, // 45-95s: every 10s - 10000, 10000, 10000, 10000, 10000, // 95-145s: every 10s - ]; - const startTime = Date.now(); - - for (let i = 0; i < intervals.length; i++) { - if (intervals[i] > 0) { - await new Promise(r => setTimeout(r, intervals[i])); - } - - const elapsedMs = Date.now() - startTime; - const elapsedSec = Math.round(elapsedMs / 1000); - - let status: string; - if (elapsedSec < 30) { - status = "Waiting for provider to accept the agreement..."; - } else if (elapsedSec < 60) { - status = "Provider is processing — this typically takes about a minute..."; - } else if (elapsedSec < 100) { - status = "Still waiting for provider acceptance..."; - } else { - status = "Taking longer than usual — provider may be busy or offline..."; - } - - console.log(`[StorageClient] waitForProvider bucket=${bucketId} attempt=${i + 1}/${intervals.length} elapsed=${elapsedSec}s`); - onProgress?.(status, elapsedMs, i + 1); - - try { - if (!this.api) throw new Error("Not connected"); - const url = await resolveProviderEndpoint(this.api, bucketId); - this.providerUrlCache.set(bucketId.toString(), url); - onProgress?.("Provider accepted — ready to use", Date.now() - startTime, intervals.length); - return url; - } catch (err) { - const msg = err instanceof Error ? err.message : ""; - const retryable = msg.includes("no primary providers") || msg.includes("not found on chain"); - if (!retryable) throw err; - if (i === intervals.length - 1) { - throw new Error( - `Provider did not accept the agreement after ${Math.round((Date.now() - startTime) / 1000)}s. ` + - `The provider may be offline or not accepting new agreements.` - ); - } - } - } - throw new Error("Provider did not accept the agreement"); - } - /** Clear cached provider URL for a bucket (e.g. after provider changes). */ invalidateProviderCache(bucketId?: bigint): void { if (bucketId !== undefined) { @@ -403,17 +493,41 @@ export class StorageClient { // --- S3 Operations --- - async createBucket(name: string, options: CreateBucketOptions): Promise { + /** + * Redeem provider-signed agreement terms on chain to open a Layer-0 + * bucket + primary agreement and register the S3 metadata bucket on top + * of it — atomically in one extrinsic. + * + * **This is only step 2 of bucket creation.** Step 1 is the HTTP + * negotiation against the provider (`negotiateTerms`). Splitting the two + * lets the caller retry a failed on-chain submit without re-negotiating + * (the signed terms are still valid until `terms.valid_until`). + * + * The provider URL is needed only so the cache can be primed for the + * first upload — the chain itself doesn't see it. + */ + async submitCreateBucket( + name: string, + providerAccount: string, + providerUrl: string, + signed: SignedTerms, + ): Promise { this.ensureConnected(); this.validateBucketName(name); - console.log("[StorageClient] createBucket:", name, options); + console.log( + "[StorageClient] submitCreateBucket:", + name, + "provider=", + providerAccount, + providerUrl, + "nonce=", + signed.terms.nonce, + ); - // Create the S3 bucket (this also creates the Layer 0 bucket). - // Provider agreement is requested separately after creation. const tx = this.api!.tx.S3Registry.create_s3_bucket({ name: Binary.fromText(name), - min_providers: 1, + ...buildSignedTermsArgs(providerAccount, signed), }); const result = await this.submitAndWatchBestBlock(tx); @@ -437,6 +551,9 @@ export class StorageClient { ); const looked = await this.headBucket(name); if (looked) { + // Prime the provider URL cache so the first upload doesn't have to + // resolve it from chain. + this.providerUrlCache.set(looked.layer0BucketId.toString(), providerUrl); return looked; } throw new Error( @@ -445,7 +562,12 @@ export class StorageClient { ); } - // Return bucket info from the event data + // We already know the provider HTTP URL — prime the cache so the first + // upload skips the on-chain lookup. + if (layer0BucketId !== null) { + this.providerUrlCache.set(layer0BucketId.toString(), providerUrl); + } + return { s3BucketId, name, @@ -939,7 +1061,7 @@ export class StorageClient { const multiaddrStr = new TextDecoder().decode(provider.multiaddr); - const url = parseMultiaddrToUrl(multiaddrStr) ?? "unknown"; + const url = parseMultiaddrToHttp(multiaddrStr) ?? "unknown"; const healthy = url !== "unknown" ? await this.probeHealthy(url) : false; results.push({ account: providerAccount, endpoint: url, healthy }); @@ -966,7 +1088,13 @@ export class StorageClient { const maxCapacity = BigInt(settings.max_capacity ?? 0); const committedBytes = BigInt(provider.committed_bytes ?? 0); - const availableCapacity = maxCapacity > committedBytes ? maxCapacity - committedBytes : 0n; + // 0 means unlimited + let availableCapacity: bigint = 0n; + + if (maxCapacity !== 0n) { + // On-chain already enforces `maxCapacity` > `committedBytes` + availableCapacity = maxCapacity - committedBytes; + } providers.push({ account, @@ -992,25 +1120,6 @@ export class StorageClient { return providers; } - async requestAgreementWithProvider( - bucketId: bigint, - providerAccount: string, - maxBytes: bigint, - duration: number, - maxPayment: bigint, - ): Promise { - this.ensureConnected(); - - const tx = this.api!.tx.StorageProvider.request_primary_agreement({ - bucket_id: bucketId, - provider: providerAccount, - max_bytes: maxBytes, - duration, - max_payment: maxPayment, - }); - - await this.submitAndWatchBestBlock(tx); - } // --- Helpers --- diff --git a/user-interfaces/console-ui/src/pages/Storage.tsx b/user-interfaces/console-ui/src/pages/Storage.tsx index 9a56e510..389734b6 100644 --- a/user-interfaces/console-ui/src/pages/Storage.tsx +++ b/user-interfaces/console-ui/src/pages/Storage.tsx @@ -68,19 +68,27 @@ export default function Storage() { {providerCapacity && (
Provider capacity: -
-
-
0n ? Number((providerCapacity.used * 100n) / providerCapacity.max) : 0}%`, - }} - /> -
- - {formatBytes(Number(providerCapacity.used))} / {formatBytes(Number(providerCapacity.max))} - -
+ { + providerCapacity.max === 0n ? ( + + Unlimited + + ) : ( +
+
+
0n ? Number((providerCapacity.used * 100n) / providerCapacity.max) : 0}%`, + }} + /> +
+ + {formatBytes(Number(providerCapacity.used))} / {formatBytes(Number(providerCapacity.max))} + +
+ ) + }
)}
diff --git a/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts b/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts new file mode 100644 index 00000000..f3e47063 --- /dev/null +++ b/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts @@ -0,0 +1,77 @@ +import { expect, type Browser, type Page } from "@playwright/test"; +import { Bob, getApi } from "@web3-storage/test-helpers"; + +/** + * Drive the drive-ui through the real user create-drive flow: + * + * 1. Click "New Drive" → form opens with the provider picker embedded. + * 2. Fill the drive name (capacity / duration / price defaults stay). + * 3. Click the first available provider's Select button — this IS the + * submit. The UI runs `POST /negotiate` then submits `create_drive` + * atomically. + * 4. Wait for the new drive id to appear under `Bob` in + * `DriveRegistry.UserDrives`. + * + * Returns the new drive's id. For tests that only need a drive as setup + * state and don't care about the UI flow, `createDriveViaApi` is faster. + */ +export async function createDriveViaUi(page: Page, name: string): Promise { + await page.getByTestId("new-drive-button").click(); + await expect(page.getByTestId("new-drive-dialog")).toBeVisible(); + await page.getByTestId("new-drive-name").fill(name); + // Capacity / duration / price-per-byte defaults are set in the component. + await expect(page.getByTestId("provider-picker")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("provider-picker-select").first().click(); + + return waitForLatestDriveId(); +} + +/** + * `beforeAll` variant — opens a fresh browser context (with the same + * localStorage seed as the `localPage` fixture: local network + Bob + * signer), navigates to the app, drives the UI create flow, and closes + * the context. Returns the new drive's id. + * + * Use from `test.beforeAll` when you need a drive created via the UI + * before any individual test runs. + */ +export async function createDriveInFreshContext( + browser: Browser, + name: string, +): Promise { + const context = await browser.newContext(); + await context.addInitScript(() => { + localStorage.setItem("web3-storage-selected-network", "local"); + localStorage.setItem("drive-ui-account-name", "Bob"); + }); + const page = await context.newPage(); + try { + await page.goto("/"); + await expect(page.getByTestId("block-number")).toBeVisible({ timeout: 30_000 }); + return await createDriveViaUi(page, name); + } finally { + await context.close(); + } +} + +/** + * Poll `DriveRegistry.UserDrives[Bob]` until it has at least one entry, + * then return the latest id. Used after the UI flow signals completion + * to bridge the UI's optimistic state and the chain's finalized state. + */ +async function waitForLatestDriveId(): Promise { + const api = getApi(); + let length = 0; + await expect + .poll( + async () => { + const ids = await api.query.DriveRegistry.UserDrives.getValue(Bob.address); + length = ids.length; + return length; + }, + { timeout: 120_000, intervals: [1000, 2000, 3000] }, + ) + .toBeGreaterThan(0); + const ids = await api.query.DriveRegistry.UserDrives.getValue(Bob.address); + return ids[ids.length - 1]; +} diff --git a/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts b/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts index 78505507..c2a27d2a 100644 --- a/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts @@ -28,10 +28,10 @@ async function fillBaseFields(page: import("@playwright/test").Page, name: strin await page.getByTestId("new-drive-button").click(); await expect(page.getByTestId("new-drive-dialog")).toBeVisible(); await page.getByTestId("new-drive-name").fill(name); - // Capacity / duration / payment / min-providers — defaults are fine for these tests. + // Capacity / duration / price-per-byte — defaults are fine for these tests. } -/** +/** * Wait for a freshly-created drive to land in Bob's UserDrives. Returns * the latest drive id. Fresh chain's first drive has id 0n which is falsy * in JS — poll on `length`, then read the id outside the poll. @@ -60,7 +60,13 @@ async function expectDriveOnChain(driveId: bigint, expectedName: string) { test("drive lands on chain with the user-supplied name", async ({ localPage }) => { const name = `create-${Date.now()}`; await fillBaseFields(localPage, name); - await localPage.getByTestId("new-drive-submit").click(); + // Provider picker is embedded in the create dialog — picking a provider + // IS the submit. globalSetup registered Alice as the lone provider, so + // the first row is always the one we want. + await expect(localPage.getByTestId("provider-picker")).toBeVisible({ + timeout: 30_000, + }); + await localPage.getByTestId("provider-picker-select").first().click(); const driveId = await waitForCreatedDriveId(); await expectDriveOnChain(driveId, name); diff --git a/user-interfaces/drive-ui/e2e/integration/file-ops.spec.ts b/user-interfaces/drive-ui/e2e/integration/file-ops.spec.ts index 112031e7..86bc1d81 100644 --- a/user-interfaces/drive-ui/e2e/integration/file-ops.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/file-ops.spec.ts @@ -6,11 +6,8 @@ * deterministic window — without the route intercept, the tests race. */ import { test, expect } from "../fixtures"; -import { - Bob, - cleanupDrives, - createDriveViaApi, -} from "@web3-storage/test-helpers"; +import { Bob, cleanupDrives } from "@web3-storage/test-helpers"; +import { createDriveViaUi } from "../helpers/createDriveViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); @@ -20,18 +17,10 @@ test.afterEach(async () => { }); async function selectFreshDrive(page: import("@playwright/test").Page) { - const drive = await createDriveViaApi(Bob, { - name: `file-ops-${Date.now()}`, - maxCapacity: 100_000_000n, - storagePeriod: 10_000, - payment: 120_000_000_000_000_000n, - minProviders: 1, - commitStrategy: { type: "Immediate" }, - }); - await page.reload(); - await page.getByTestId(`drive-list-item-${drive.driveId}`).click(); + const driveId = await createDriveViaUi(page, `file-ops-${Date.now()}`); + await page.getByTestId(`drive-list-item-${driveId}`).click(); await expect(page.getByTestId("file-browser")).toBeVisible(); - return drive; + return { driveId }; } test("multi-file upload appears in entries table", async ({ localPage }) => { diff --git a/user-interfaces/drive-ui/e2e/integration/members.spec.ts b/user-interfaces/drive-ui/e2e/integration/members.spec.ts index 90e933ed..cca7e8a9 100644 --- a/user-interfaces/drive-ui/e2e/integration/members.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/members.spec.ts @@ -15,24 +15,17 @@ import { Bob, Charlie, cleanupDrives, - createDriveViaApi, } from "@web3-storage/test-helpers"; +import { createDriveInFreshContext } from "../helpers/createDriveViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); let driveId: bigint; -test.beforeAll(async () => { +test.beforeAll(async ({ browser }) => { test.setTimeout(120_000); - const drive = await createDriveViaApi(Bob, { - name: `members-${Date.now()}`, - maxCapacity: 10_000_000n, - storagePeriod: 10_000, - payment: 120_000_000_000_000_000n, - minProviders: 1, - }); - driveId = drive.driveId; + driveId = await createDriveInFreshContext(browser, `members-${Date.now()}`); }); test.afterAll(async () => { diff --git a/user-interfaces/drive-ui/e2e/integration/persistence.spec.ts b/user-interfaces/drive-ui/e2e/integration/persistence.spec.ts index a256ed98..87e7d2d1 100644 --- a/user-interfaces/drive-ui/e2e/integration/persistence.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/persistence.spec.ts @@ -10,30 +10,30 @@ * (saves ~30s of provider settlement per duplicate create). */ import { test, expect } from "../fixtures"; -import { Bob, createDriveViaApi, cleanupDrives, type DriveHandle } from "@web3-storage/test-helpers"; +import { Bob, cleanupDrives } from "@web3-storage/test-helpers"; +import { createDriveInFreshContext } from "../helpers/createDriveViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); -let sharedDrive: DriveHandle | null = null; +let sharedDriveId: bigint | null = null; + +test.beforeAll(async ({ browser }) => { + test.setTimeout(120_000); + sharedDriveId = await createDriveInFreshContext(browser, `persistence-${Date.now()}`); +}); test.afterAll(async () => { test.setTimeout(60_000); await cleanupDrives(Bob); - sharedDrive = null; + sharedDriveId = null; }); -async function getOrCreateDrive(): Promise { - if (sharedDrive) return sharedDrive; - sharedDrive = await createDriveViaApi(Bob, { - name: `persistence-${Date.now()}`, - maxCapacity: 10_000_000n, - storagePeriod: 10_000, - payment: 120_000_000_000_000_000n, - minProviders: 1, - commitStrategy: { type: "Batched", interval: 100 }, - }); - return sharedDrive; +function requireDriveId(): bigint { + if (sharedDriveId === null) { + throw new Error("sharedDriveId not initialized — beforeAll did not run"); + } + return sharedDriveId; } test("endpoint selection persists across reload", async ({ localPage }) => { @@ -63,28 +63,28 @@ test("account name persists across reload", async ({ localPage }) => { test("view mode toggle persists across reload", async ({ localPage }) => { // The view mode toggle is in the file-browser; only renders when a drive is // selected. - const drive = await getOrCreateDrive(); + const driveId = requireDriveId(); await localPage.reload(); - await localPage.getByTestId(`drive-list-item-${drive.driveId}`).click(); + await localPage.getByTestId(`drive-list-item-${driveId}`).click(); const toggle = localPage.getByTestId("view-mode-toggle"); await expect(toggle).toBeVisible(); // Capture current mode (text varies per toggle UI), click to toggle, reload, verify. await toggle.click(); await localPage.reload(); - await localPage.getByTestId(`drive-list-item-${drive.driveId}`).click(); + await localPage.getByTestId(`drive-list-item-${driveId}`).click(); // After reload, the same drive should be selected (selected-drive persists too). - await expect(localPage.getByTestId(`drive-list-item-${drive.driveId}`)).toBeVisible(); + await expect(localPage.getByTestId(`drive-list-item-${driveId}`)).toBeVisible(); // localStorage drive-ui-view-mode should be "grid" after one toggle from default "list". const stored = await localPage.evaluate(() => localStorage.getItem("drive-ui-view-mode")); expect(stored === "grid" || stored === "list").toBe(true); }); test("selected drive persists across reload", async ({ localPage }) => { - const drive = await getOrCreateDrive(); + const driveId = requireDriveId(); await localPage.reload(); - await localPage.getByTestId(`drive-list-item-${drive.driveId}`).click(); + await localPage.getByTestId(`drive-list-item-${driveId}`).click(); await expect(localPage.getByTestId("file-browser")).toBeVisible(); await localPage.reload(); // After reload, file-browser should still be visible (drive auto-selected). @@ -92,5 +92,5 @@ test("selected drive persists across reload", async ({ localPage }) => { const stored = await localPage.evaluate(() => localStorage.getItem("drive-ui-selected-drive"), ); - expect(stored).toBe(drive.driveId.toString()); + expect(stored).toBe(driveId.toString()); }); diff --git a/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts b/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts index 4aa9c537..1672a972 100644 --- a/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts @@ -9,11 +9,11 @@ import { test, expect } from "../fixtures"; import { Bob, cleanupDrives, - createDriveViaApi, deleteDriveViaApi, waitForConnection, waitForMinBlock, } from "@web3-storage/test-helpers"; +import { createDriveViaUi } from "../helpers/createDriveViaUi"; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); @@ -41,20 +41,12 @@ async function openTabB(browser: import("@playwright/test").Browser) { test("DriveCreated cross-tab", async ({ localPage, browser }) => { const { tabB, ctx } = await openTabB(browser); try { - const drive = await createDriveViaApi(Bob, { - name: `rt-create-${Date.now()}`, - maxCapacity: 10_000_000n, - storagePeriod: 10_000, - payment: 120_000_000_000_000_000n, - minProviders: 1, - }); + const driveId = await createDriveViaUi(localPage, `rt-create-${Date.now()}`); - // Tab A also reflects (sanity). - await expect(localPage.getByTestId(`drive-list-item-${drive.driveId}`)).toBeVisible({ + await expect(localPage.getByTestId(`drive-list-item-${driveId}`)).toBeVisible({ timeout: 90_000, }); - // Tab B reflects without reload. - await expect(tabB.getByTestId(`drive-list-item-${drive.driveId}`)).toBeVisible({ + await expect(tabB.getByTestId(`drive-list-item-${driveId}`)).toBeVisible({ timeout: 90_000, }); } finally { @@ -65,23 +57,17 @@ test("DriveCreated cross-tab", async ({ localPage, browser }) => { test("DriveDeleted cross-tab", async ({ localPage, browser }) => { const { tabB, ctx } = await openTabB(browser); try { - const drive = await createDriveViaApi(Bob, { - name: `rt-delete-${Date.now()}`, - maxCapacity: 10_000_000n, - storagePeriod: 10_000, - payment: 120_000_000_000_000_000n, - minProviders: 1, - }); - await expect(tabB.getByTestId(`drive-list-item-${drive.driveId}`)).toBeVisible({ + const driveId = await createDriveViaUi(localPage, `rt-delete-${Date.now()}`); + await expect(tabB.getByTestId(`drive-list-item-${driveId}`)).toBeVisible({ timeout: 90_000, }); - await deleteDriveViaApi(Bob, drive.driveId); + await deleteDriveViaApi(Bob, driveId); - await expect(tabB.getByTestId(`drive-list-item-${drive.driveId}`)).toBeHidden({ + await expect(tabB.getByTestId(`drive-list-item-${driveId}`)).toBeHidden({ timeout: 90_000, }); - await expect(localPage.getByTestId(`drive-list-item-${drive.driveId}`)).toBeHidden({ + await expect(localPage.getByTestId(`drive-list-item-${driveId}`)).toBeHidden({ timeout: 90_000, }); } finally { diff --git a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx index bcdc2221..66b107ba 100644 --- a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx +++ b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Loader2, CheckCircle2, XCircle, RefreshCw, Clock, AlertTriangle } from "lucide-react"; +import { CheckCircle2, XCircle, RefreshCw } from "lucide-react"; import { Dialog, DialogContent, @@ -10,35 +10,32 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { + canRetryCreation, createDrive, - useCreations, dismissCreation, + retryCreation, + useCreations, type CreationStatus, } from "@/state"; +import type { AvailableProvider } from "@/lib/drive-client"; import { formatBytes } from "@/lib/utils"; +import ProviderPickerPanel from "./ProviderPickerPanel"; interface NewDriveDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } -function formatElapsed(ms: number): string { - const sec = Math.round(ms / 1000); - if (sec < 60) return `${sec}s`; - const min = Math.floor(sec / 60); - const rem = sec % 60; - return `${min}m ${rem}s`; -} - function CreationStatusCard({ item, onDismiss, + onRetry, }: { item: CreationStatus; onDismiss: (id: string) => void; + onRetry?: (id: string) => void; }) { const isDismissible = item.stage === "ready" || item.stage === "failed"; - const showElapsed = item.stage === "waiting" || item.stage === "created"; return (
-
-

{item.name}

- {showElapsed && ( - - - {formatElapsed(item.elapsedMs)} - - )} -
+

{item.name}

- {item.stage === "submitting" && "Submitting transaction to blockchain..."} - {item.stage === "created" && "Drive created on-chain. Setting up storage agreement..."} - {item.stage === "waiting" && (item.statusMessage || "Waiting for provider to accept...")} + {item.stage === "submitting" && "Negotiating with provider and submitting on-chain..."} {item.stage === "ready" && "Drive is ready to use"} {item.stage === "failed" && (item.error || "Something went wrong")}

- {item.stage === "waiting" && item.elapsedMs > 60000 && ( -
- - Taking longer than expected + {item.stage === "failed" && onRetry && ( +
+
)}
@@ -103,125 +96,124 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro const creations = useCreations(); const [name, setName] = useState(""); - const [capacity, setCapacity] = useState("10000000"); + const [capacity, setCapacity] = useState("10485760"); const [duration, setDuration] = useState("10000"); - const [payment, setPayment] = useState("120000000000000000"); - const [minProviders, setMinProviders] = useState("1"); - const [creating, setCreating] = useState(false); + const [pricePerByte, setPricePerByte] = useState("0"); + const [submitting, setSubmitting] = useState(false); - const handleCreate = async () => { - setCreating(true); + /** + * Clicking a provider's Select button IS the submit action. Hand the + * form values + picked provider to the state hook, which runs + * `negotiate → submit_create_drive` atomically. + */ + const handleProviderSelect = async (provider: AvailableProvider) => { + setSubmitting(true); try { - await createDrive({ + const drive = await createDrive({ name: name || undefined, maxCapacity: BigInt(capacity), storagePeriod: parseInt(duration, 10), - payment: BigInt(payment), - minProviders: Math.max(1, Math.min(10, parseInt(minProviders, 10) || 1)), + pricePerByte: BigInt(pricePerByte || "0"), + provider, }); - setName(""); - onOpenChange(false); - } catch { - /* error handled in state via creations$ */ + if (drive) { + setName(""); + onOpenChange(false); + } } finally { - setCreating(false); + setSubmitting(false); } }; return ( - - - - Create New Drive - - Set up a new decentralized drive for your files. - - -
-
- - setName(e.target.value)} - placeholder="My Documents" - /> -
- -
-
- - setCapacity(e.target.value)} - /> -

- {formatBytes(parseInt(capacity, 10) || 0)} -

-
-
- - setDuration(e.target.value)} - /> -
-
- - setPayment(e.target.value)} - /> -
-
- + <> + + + + Create New Drive + + Set up a new decentralized drive for your files. + + +
+
+ setMinProviders(e.target.value)} + data-testid="new-drive-name" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="My Documents" /> -

1–10 required

-
- {creations.length > 0 && ( -
- {creations.map((item) => ( - +
+ + setCapacity(e.target.value)} + /> +

+ {formatBytes(parseInt(capacity, 10) || 0)} +

+
+
+ + setDuration(e.target.value)} + /> +
+
+ + setPricePerByte(e.target.value)} /> - ))} +
- )} -
- - + + + {creations.length > 0 && ( +
+ {creations.map((item) => ( + { + void retryCreation(id); + } + : undefined + } + /> + ))} +
+ )} + +
+ +
-
- -
+ + + ); } diff --git a/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx b/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx new file mode 100644 index 00000000..285138a1 --- /dev/null +++ b/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx @@ -0,0 +1,177 @@ +import { useState, useEffect } from "react"; +import { RefreshCw, Server } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { listAvailableProviders } from "@/state"; +import type { AvailableProvider } from "@/lib/drive-client"; +import { formatBytes, truncateHash, formatTokens } from "@/lib/utils"; + +interface ProviderPickerPanelProps { + onSelect: (provider: AvailableProvider) => void; + requiredCapacity: bigint; + requiredDuration: number; + /** Disable all Select buttons (e.g. while a submission is in flight). */ + disabled?: boolean; +} + +/** + * Inline panel that lists registered storage providers. Designed to be + * embedded inside the Create New Drive dialog so the user picks a + * provider as part of the same form — no separate modal. + * + * Mirrors console-ui's ProviderPickerDialog table — same `data-testid`s + * (`provider-picker`, `provider-picker-select`) so shared e2e patterns + * address both UIs. + */ +export default function ProviderPickerPanel({ + onSelect, + requiredCapacity, + requiredDuration, + disabled = false, +}: ProviderPickerPanelProps) { + const [providers, setProviders] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + loadProviders(); + }, []); + + const loadProviders = async () => { + setLoading(true); + setError(null); + try { + const list = await listAvailableProviders(); + setProviders(list); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load providers"); + } finally { + setLoading(false); + } + }; + + const getDisabledReason = (p: AvailableProvider): string | null => { + if (!p.acceptingPrimary) return "Not accepting"; + // `maxCapacity === 0n` means unlimited — skip the capacity check. + if (p.maxCapacity !== 0n && p.availableCapacity < requiredCapacity) + return "Capacity full"; + if (requiredDuration < p.minDuration) return "Duration too short"; + if (p.maxDuration > 0 && requiredDuration > p.maxDuration) + return "Duration too long"; + return null; + }; + + return ( +
+
+ + +
+ + {loading ? ( +
+ + Loading providers... +
+ ) : error ? ( +
+

{error}

+ +
+ ) : providers.length === 0 ? ( +
+ +

No providers registered on chain

+
+ ) : ( +
+ + + + + + + + + + + + {providers.map((p) => { + const reason = getDisabledReason(p); + const rowDisabled = reason !== null; + // `max_capacity == 0` is the substrate convention for + // "unlimited" (see runtime ProviderSettings docs). + const unlimited = p.maxCapacity === 0n; + const utilization = unlimited + ? 0 + : Number( + ((p.maxCapacity - p.availableCapacity) * 100n) / p.maxCapacity, + ); + return ( + + + + + + + + ); + })} + +
AccountAvailablePrice/byteDurationAction
+ + {truncateHash(p.account, 8, 6)} + + + {unlimited ? ( + Unlimited + ) : ( +
+ + {formatBytes(Number(p.availableCapacity))} + +
+
+
+
+ )} +
+ {formatTokens(p.pricePerByte)} + + {p.minDuration}–{p.maxDuration || "∞"} + + {rowDisabled ? ( + {reason} + ) : ( + + )} +
+
+ )} +
+ ); +} diff --git a/user-interfaces/drive-ui/src/lib/drive-client.ts b/user-interfaces/drive-ui/src/lib/drive-client.ts index d2d5e47c..1891341f 100644 --- a/user-interfaces/drive-ui/src/lib/drive-client.ts +++ b/user-interfaces/drive-ui/src/lib/drive-client.ts @@ -25,7 +25,45 @@ export interface DriveInfo { createdAt: number; storagePeriod: number; expiresAt: number; - payment: bigint; +} + +/** + * Provider-signed agreement terms returned by `POST /negotiate` on the + * provider node. The signature is the SCALE-encoded `MultiSignature` as + * hex (e.g. `0x01<64-byte-sr25519-sig>`). + */ +export interface SignedTerms { + terms: { + owner: string; + max_bytes: number | bigint; + duration: number; + price_per_byte: number | bigint; + valid_until: number; + nonce: number | bigint; + replica_params: unknown | null; + }; + signature: string; +} + +export interface NegotiateRequest { + owner: string; + max_bytes: number | bigint; + duration: number; + price_per_byte: number | bigint; + replica_params: unknown | null; +} + +export interface AvailableProvider { + account: string; + multiaddr: string; + stake: bigint; + availableCapacity: bigint; + maxCapacity: bigint; + pricePerByte: bigint; + minDuration: number; + maxDuration: number; + acceptingPrimary: boolean; + agreementsTotal: number; } export interface FsEntry { @@ -40,8 +78,8 @@ export interface CreateDriveOptions { name?: string; maxCapacity: bigint; storagePeriod: number; - payment: bigint; - minProviders?: number; + /** Price per byte per block. Defaults to 0 if omitted. */ + pricePerByte?: bigint; } export type MemberRole = "Admin" | "Writer" | "Reader"; @@ -117,6 +155,110 @@ function decodeName(name: Uint8Array | undefined): string | null { } } +/** + * POST a `NegotiateRequest` to the provider's `/negotiate` endpoint and + * return the provider-signed terms bundle. Mirrors + * `console-ui/src/lib/storage.ts::negotiateTerms`. + */ +export async function negotiateTerms( + providerUrl: string, + request: NegotiateRequest, +): Promise { + const res = await fetch(`${providerUrl.replace(/\/$/, "")}/negotiate`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request, (_k, v) => + typeof v === "bigint" ? v.toString() : v, + ), + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`/negotiate failed: ${res.status} ${body}`); + } + return res.json(); +} + +// MultiSignature SCALE variant order from sp_runtime. +const MULTI_SIGNATURE_VARIANT: Record = { + 0: "Ed25519", + 1: "Sr25519", + 2: "Ecdsa", + 3: "Eth", +}; + +function hexToBytes(hex: string): Uint8Array { + const h = hex.startsWith("0x") ? hex.slice(2) : hex; + const out = new Uint8Array(h.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16); + } + return out; +} + +/** + * Build the `{ provider, terms, sig }` args shared by every signed-terms + * extrinsic. The inner of `MultiSignature::Sr25519` is `[u8; 64]` which + * PAPI v2 encodes as `SizedBytes(64) = Codec` — pass a + * `0x`-prefixed hex string, NOT a `Uint8Array`. Mirrors + * `console-ui/src/lib/storage.ts::buildSignedTermsArgs`. + */ +export function buildSignedTermsArgs( + providerAccount: string, + signed: SignedTerms, +) { + const sigBytes = hexToBytes(signed.signature); + if (sigBytes.length < 1) { + throw new Error("signature too short to contain a MultiSignature variant byte"); + } + const variantName = MULTI_SIGNATURE_VARIANT[sigBytes[0]]; + if (!variantName) { + throw new Error(`unknown MultiSignature variant byte: ${sigBytes[0]}`); + } + const sigPayloadHex = + "0x" + + Array.from(sigBytes.slice(1)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sig = Enum(variantName as any, sigPayloadHex); + + const t = signed.terms; + const terms = { + owner: t.owner, + max_bytes: BigInt(t.max_bytes), + duration: t.duration, + price_per_byte: BigInt(t.price_per_byte), + valid_until: t.valid_until, + nonce: BigInt(t.nonce), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + replica_params: (t.replica_params ?? undefined) as any, + }; + return { provider: providerAccount, terms, sig }; +} + +export function parseMultiaddrToHttp(multiaddr: string): string | null { + const parts = multiaddr.split("/").filter(Boolean); + let host: string | null = null; + let port: string | null = null; + + for (let i = 0; i < parts.length; i++) { + const seg = parts[i]; + const next = parts[i + 1]; + if (!next) continue; + + if ((seg === "ip4" || seg === "ip6" || seg === "dns4" || seg === "dns6") && host === null) { + host = seg.startsWith("ip6") ? `[${next}]` : next; + } + if (seg === "tcp" && port === null) { + port = next; + } + if (host !== null && port !== null) break; + } + + if (host && port) return `http://${host}:${port}`; + return null; +} + export class DriveClient { private api: ParachainApi | null = null; private signer: Signer | null = null; @@ -199,52 +341,49 @@ export class DriveClient { this.providerUrlCache.delete(bucketId.toString()); } - async waitForProvider( - bucketId: bigint, - onProgress?: (status: string, elapsedMs: number) => void, - ): Promise { - this.invalidateProviderUrl(bucketId); - - const intervals = [ - 0, 3000, 3000, 3000, 3000, 3000, 6000, 6000, 6000, 6000, 6000, - 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - ]; - const startTime = Date.now(); - - for (let i = 0; i < intervals.length; i++) { - if (intervals[i] > 0) await sleep(intervals[i]); - - const elapsedMs = Date.now() - startTime; - const elapsedSec = Math.round(elapsedMs / 1000); - - let status: string; - if (elapsedSec < 15) status = "Checking if provider has accepted the agreement..."; - else if (elapsedSec < 45) status = "Provider is reviewing the agreement..."; - else if (elapsedSec < 90) status = "Still waiting for provider to accept..."; - else status = "Taking longer than usual — provider may be busy or offline..."; - - onProgress?.(status, elapsedMs); - - try { - const url = await resolveProviderEndpoint(this.requireApi(), bucketId); - this.providerUrlCache.set(bucketId.toString(), url); - onProgress?.("Provider accepted — ready to use", Date.now() - startTime); - return url; - } catch (err) { - const retryable = - err instanceof Error && - (err.message.includes("no primary providers") || err.message.includes("not found on chain")); - if (!retryable) throw err; - if (i === intervals.length - 1) { - throw new Error( - `Provider did not accept the agreement after ${Math.round( - (Date.now() - startTime) / 1000, - )}s. The provider may be offline or not accepting new agreements.`, - ); - } - } + /** + * Walk `StorageProvider.Providers` storage and return all registered + * providers with their settings, sorted by free capacity descending. + * Used by the provider picker to surface candidates before negotiation. + */ + async listAvailableProviders(): Promise { + const api = this.requireApi(); + const entries = await api.query.StorageProvider.Providers.getEntries(); + const providers: AvailableProvider[] = []; + + for (const entry of entries) { + const provider = entry.value; + const account = entry.keyArgs[0] as string; + const settings = provider.settings; + + const multiaddrStr = new TextDecoder().decode(provider.multiaddr); + const maxCapacity = BigInt(settings.max_capacity ?? 0); + const committedBytes = BigInt(provider.committed_bytes ?? 0); + const availableCapacity = + maxCapacity > committedBytes ? maxCapacity - committedBytes : 0n; + + providers.push({ + account, + multiaddr: multiaddrStr, + stake: BigInt(provider.stake ?? 0), + availableCapacity, + maxCapacity, + pricePerByte: BigInt(settings.price_per_byte ?? 0), + minDuration: settings.min_duration ?? 0, + maxDuration: settings.max_duration ?? 0, + acceptingPrimary: settings.accepting_primary ?? false, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + agreementsTotal: (provider.stats as any)?.agreements_total ?? 0, + }); } - throw new Error("Provider did not accept the agreement"); + + providers.sort((a, b) => { + if (b.availableCapacity > a.availableCapacity) return 1; + if (b.availableCapacity < a.availableCapacity) return -1; + return 0; + }); + + return providers; } // ── Account ─────────────────────────────────────────────────────────────── @@ -257,18 +396,28 @@ export class DriveClient { // ── Drive on-chain operations ───────────────────────────────────────────── - async createDrive(options: CreateDriveOptions): Promise { + /** + * Redeem provider-signed agreement terms on chain to open a Layer-0 + * bucket + primary agreement and register the drive on top — atomically + * in one extrinsic. + * + * **Step 2 of drive creation.** Step 1 is the HTTP `negotiateTerms` call + * against the chosen provider. Splitting the two lets a failed on-chain + * submit be retried without re-negotiating (terms valid until + * `terms.valid_until`). + */ + async submitCreateDrive( + name: string | undefined, + providerAccount: string, + providerUrl: string, + signed: SignedTerms, + ): Promise { const api = this.requireApi(); const { address } = this.requireSigner(); - const nameArg = options.name ? Binary.fromText(options.name) : undefined; - const tx = api.tx.DriveRegistry.create_drive({ - name: nameArg, - max_capacity: options.maxCapacity, - storage_period: options.storagePeriod, - payment: options.payment, - min_providers: options.minProviders ?? undefined, + name: name ? Binary.fromText(name) : undefined, + ...buildSignedTermsArgs(providerAccount, signed), }); const result = await this.submit(tx); @@ -276,23 +425,30 @@ export class DriveClient { const created = api.event.DriveRegistry.DriveCreated.filter(result.events); if (created.length === 0) { const drives = await this.listDrives(); - if (drives.length > 0) return drives[drives.length - 1]; + if (drives.length > 0) { + // Prime the provider URL cache so the first upload skips the on-chain lookup. + this.providerUrlCache.set(drives[drives.length - 1].bucketId.toString(), providerUrl); + return drives[drives.length - 1]; + } throw new Error( "DriveCreated event not found. The runtime descriptor may be stale — run: pnpm papi:generate", ); } const { drive_id, bucket_id } = created[0].payload; + // We already know the provider HTTP URL — prime the cache so the first + // upload doesn't have to resolve it from chain. + this.providerUrlCache.set(bucket_id.toString(), providerUrl); + return { driveId: drive_id, bucketId: bucket_id, owner: address, - name: options.name ?? null, - maxCapacity: options.maxCapacity, + name: name ?? null, + maxCapacity: BigInt(signed.terms.max_bytes), createdAt: 0, - storagePeriod: options.storagePeriod, + storagePeriod: signed.terms.duration, expiresAt: 0, - payment: options.payment, }; } @@ -316,7 +472,6 @@ export class DriveClient { createdAt: drive.created_at, storagePeriod: drive.storage_period, expiresAt: drive.expires_at, - payment: drive.payment, }); } return drives; @@ -335,7 +490,6 @@ export class DriveClient { createdAt: drive.created_at, storagePeriod: drive.storage_period, expiresAt: drive.expires_at, - payment: drive.payment, }; } diff --git a/user-interfaces/drive-ui/src/state/drive.state.ts b/user-interfaces/drive-ui/src/state/drive.state.ts index 2c4a5069..2901818e 100644 --- a/user-interfaces/drive-ui/src/state/drive.state.ts +++ b/user-interfaces/drive-ui/src/state/drive.state.ts @@ -10,9 +10,12 @@ import { BehaviorSubject, combineLatest, distinctUntilChanged, Subscription } fr import { bind } from "@react-rxjs/core"; import { DriveClient, + negotiateTerms, + parseMultiaddrToHttp, + type AvailableProvider, type DriveInfo, type FsEntry, - type CreateDriveOptions, + type SignedTerms, } from "@/lib/drive-client"; import { api$$, getApi } from "@/state/chain.state"; import { signer$$, getSignerAddress, refreshBalance } from "@/state/wallet.state"; @@ -21,18 +24,26 @@ import { signer$$, getSignerAddress, refreshBalance } from "@/state/wallet.state // Types // ───────────────────────────────────────────────────────────────────────────── -export type CreationStage = "submitting" | "created" | "waiting" | "ready" | "failed"; +export type CreationStage = "submitting" | "ready" | "failed"; export interface CreationStatus { id: string; name: string; stage: CreationStage; - statusMessage?: string; elapsedMs: number; error?: string; bucketId?: bigint; } +export interface CreateDriveInput { + name?: string; + maxCapacity: bigint; + storagePeriod: number; + pricePerByte?: bigint; + /** The provider the user picked. */ + provider: AvailableProvider; +} + export type ViewMode = "list" | "grid"; // ───────────────────────────────────────────────────────────────────────────── @@ -294,46 +305,112 @@ export function dismissCreation(id: string): void { creations$.next(creations$.getValue().filter((c) => c.id !== id)); } -export async function createDrive(options: CreateDriveOptions): Promise { +interface RetryCtx { + name: string | undefined; + provider: AvailableProvider; + url: string; + signed: SignedTerms; +} +const retryCtx = new Map(); + +export function canRetryCreation(id: string): boolean { + return retryCtx.has(id); +} + +async function runChainSubmit(id: string, ctx: RetryCtx): Promise { + updateCreation(id, { stage: "submitting", error: undefined }); + try { + const drive = await client.submitCreateDrive(ctx.name, ctx.provider.account, ctx.url, ctx.signed); + updateCreation(id, { stage: "ready", bucketId: drive.bucketId }); + retryCtx.delete(id); + await refreshDrives(); + const refreshed = drives$.getValue().find((d) => d.driveId === drive.driveId) ?? drive; + await selectDrive(refreshed); + await refreshBalance(); + return refreshed; + } catch (err) { + updateCreation(id, { + stage: "failed", + error: err instanceof Error ? err.message : "Failed to submit on chain", + }); + return null; + } +} + +export async function createDrive(input: CreateDriveInput): Promise { if (!client.hasApi() || !client.hasSigner()) return null; + const url = parseMultiaddrToHttp(input.provider.multiaddr); + if (!url) { + const id = crypto.randomUUID(); + creations$.next([ + ...creations$.getValue(), + { + id, + name: input.name || "Untitled Drive", + stage: "failed", + elapsedMs: 0, + error: `Provider ${input.provider.account} has an unparseable multiaddr: ${input.provider.multiaddr}`, + }, + ]); + return null; + } + const id = crypto.randomUUID(); - const name = options.name || "Untitled Drive"; + const displayName = input.name || "Untitled Drive"; creations$.next([ ...creations$.getValue(), - { id, name, stage: "submitting", elapsedMs: 0 }, + { id, name: displayName, stage: "submitting", elapsedMs: 0 }, ]); - try { - const drive = await client.createDrive(options); - updateCreation(id, { stage: "created", bucketId: drive.bucketId }); + const ownerAddress = client.getSignerAddress(); + if (!ownerAddress) { + updateCreation(id, { stage: "failed", error: "Signer not set" }); + return null; + } - updateCreation(id, { stage: "waiting" }); - try { - await client.waitForProvider(drive.bucketId, (status, elapsedMs) => { - updateCreation(id, { statusMessage: status, elapsedMs }); - }); - updateCreation(id, { stage: "ready" }); - await refreshDrives(); - const refreshed = drives$.getValue().find((d) => d.driveId === drive.driveId) ?? drive; - await selectDrive(refreshed); - await refreshBalance(); - return refreshed; - } catch (err) { - updateCreation(id, { - stage: "failed", - error: err instanceof Error ? err.message : "Provider did not accept", - }); - await refreshDrives(); - return null; - } + // Phase A: negotiate. Failure here means re-negotiate from scratch on retry. + let signed: SignedTerms; + try { + signed = await negotiateTerms(url, { + owner: ownerAddress, + max_bytes: input.maxCapacity, + duration: input.storagePeriod, + price_per_byte: input.pricePerByte ?? 0n, + replica_params: null, + }); } catch (err) { updateCreation(id, { stage: "failed", - error: err instanceof Error ? err.message : "Failed to create drive", + error: err instanceof Error ? err.message : "Failed to negotiate with provider", }); return null; } + + // Phase B: chain submit. Stash retry context first so a failure leaves + // a retry handle attached to the CreationStatus. + const ctx: RetryCtx = { name: input.name, provider: input.provider, url, signed }; + retryCtx.set(id, ctx); + return runChainSubmit(id, ctx); +} + +/** + * Retry a failed on-chain submit using the cached signed terms. No-op if + * the creation expired or never negotiated successfully. + */ +export async function retryCreation(id: string): Promise { + const ctx = retryCtx.get(id); + if (!ctx) return null; + return runChainSubmit(id, ctx); +} + +/** + * Walk on-chain provider state and return the list of registered + * providers. The picker dialog consumes this before negotiation. + */ +export async function listAvailableProviders(): Promise { + if (!client.hasApi()) return []; + return client.listAvailableProviders(); } export async function deleteDrive(driveId: bigint): Promise { diff --git a/user-interfaces/drive-ui/src/state/index.ts b/user-interfaces/drive-ui/src/state/index.ts index f76a285f..ecde96de 100644 --- a/user-interfaces/drive-ui/src/state/index.ts +++ b/user-interfaces/drive-ui/src/state/index.ts @@ -69,6 +69,9 @@ export { deleteEntry, createFolder, createDrive, + retryCreation, + canRetryCreation, + listAvailableProviders, deleteDrive, fetchMembers, addMember, @@ -78,7 +81,12 @@ export { getSelectedDrive, getCurrentPath, } from "./drive.state"; -export type { CreationStage, CreationStatus, ViewMode } from "./drive.state"; +export type { + CreationStage, + CreationStatus, + CreateDriveInput, + ViewMode, +} from "./drive.state"; export { useCheckpointInfo, diff --git a/user-interfaces/pnpm-lock.yaml b/user-interfaces/pnpm-lock.yaml index 347f0ebb..05861db9 100644 --- a/user-interfaces/pnpm-lock.yaml +++ b/user-interfaces/pnpm-lock.yaml @@ -94,7 +94,7 @@ importers: version: 1.59.1 '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 4.2.4(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -103,7 +103,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 6.0.1(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@web3-storage/test-helpers': specifier: workspace:* version: link:../shared/test-helpers @@ -130,7 +130,7 @@ importers: version: 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) vite: specifier: ^8.0.10 - version: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + version: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) drive-ui: dependencies: @@ -215,7 +215,7 @@ importers: version: 1.59.1 '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 4.2.4(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -224,7 +224,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 6.0.1(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@web3-storage/test-helpers': specifier: workspace:* version: link:../shared/test-helpers @@ -236,10 +236,10 @@ importers: version: 5.8.3 vite: specifier: ^8.0.10 - version: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + version: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 4.1.5(@types/node@25.9.1)(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) provider: dependencies: @@ -324,7 +324,7 @@ importers: version: 1.59.1 '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 4.2.4(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -333,7 +333,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 6.0.1(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@web3-storage/test-helpers': specifier: workspace:* version: link:../shared/test-helpers @@ -366,10 +366,10 @@ importers: version: 8.59.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) vite: specifier: ^8.0.10 - version: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + version: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + version: 4.1.5(@types/node@25.9.1)(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) shared/network-config: {} @@ -402,8 +402,8 @@ importers: version: 2.1.1(esbuild@0.28.0)(rxjs@7.8.2) devDependencies: '@polkadot-api/cli': - specifier: ^0.20.4 - version: 0.20.4(esbuild@0.28.0) + specifier: ^0.21.3 + version: 0.21.3(esbuild@0.28.0) shared/papi/.papi/descriptors: dependencies: @@ -752,26 +752,26 @@ packages: engines: {node: '>=18'} hasBin: true - '@polkadot-api/cli@0.20.4': - resolution: {integrity: sha512-YYSeHB0Aqf2PSV4giaFPWO9C9gqv9WRmeS2rXkwOoi5lLmnw1K+WgY1d5zk9aNVbRT818LOV74X0HJJW0c5dxw==} - hasBin: true - '@polkadot-api/cli@0.21.0': resolution: {integrity: sha512-drFsvCOnF830hJsabZkrHdCp67ZRBXHf41N7yvqeOkvXjgMS7oQOqRlC6UjF3ILOli/oi3I5UOoPUjH2jjZjEA==} hasBin: true - '@polkadot-api/codegen@0.22.3': - resolution: {integrity: sha512-TaJpxUOuXFIgrkjhPw/H0fwyKz74R/NL4W3Z0YPkxa89fvMmUqlvnam4e7GzcuR7glcXVTELYY6miCEsg4f+Eg==} + '@polkadot-api/cli@0.21.3': + resolution: {integrity: sha512-illlEy+fYdNhU5FGjNrVtcPzSdCl6O24bT/JMEbrRHicckLiq07oyOcZ7O4T7FEg094zgXolg8K/1ro5ESunUw==} + hasBin: true '@polkadot-api/codegen@0.22.4': resolution: {integrity: sha512-SHab4/zICOMzIwuZai3SBiKnWz62eEYCHOCvt6zSbAMddNGg3HgXANG0IEBVM5Ad+wX9YFIINAFkV8y1Pbwv5g==} - '@polkadot-api/ink-contracts@0.6.1': - resolution: {integrity: sha512-zBuvNSO8V8HLhhWeHvavLJDLrnHNauqfzzvZpqpxVDKEA/dEiyJkZOOtK8OuNdeipCseokwaOyIukTESQGQijg==} + '@polkadot-api/codegen@0.22.5': + resolution: {integrity: sha512-zwZJAlviI211zhj5i6oXkrr0crrbO3GZjBd2vC9AshY1pRmyiNLV9DZsXw4E6l4JQwdAAyNWtPdnvvB9T3TpKg==} '@polkadot-api/ink-contracts@0.6.2': resolution: {integrity: sha512-RWsXlT6SuY1oCRfcPrQLGe2emE9Fz6pdQ5QhHvC/OdsVHl3tDCVHx384CRSoxasAb1rucok8pGhpYc0IUzq2Jw==} + '@polkadot-api/ink-contracts@0.6.3': + resolution: {integrity: sha512-XqnM1VDzI5L62xgg+f8le2yEoz8QZbUKEfAfPnHMOgBj9tJiyF15FcOJmnLMO/vq3cGixqh18tzJekLG5YTxtA==} + '@polkadot-api/json-rpc-provider-proxy@0.1.0': resolution: {integrity: sha512-8GSFE5+EF73MCuLQm8tjrbCqlgclcHBSRaswvXziJ0ZW7iw3UEMsKkkKvELayWyBuOPa2T5i1nj6gFOeIsqvrg==} @@ -784,43 +784,43 @@ packages: '@polkadot-api/json-rpc-provider@0.2.0': resolution: {integrity: sha512-lhkuBS/x06i3djQIN7p8jVkMnYuGsUAEMS6RhdWeEpz7X/8/APER4Wdih7MEBovCuwVSCTjOxl8f+alH7AZHZg==} - '@polkadot-api/known-chains@0.11.2': - resolution: {integrity: sha512-AlOpIbKLcilTf87/unNuZaNQG6IFr9QrP3i7p9SCU1LO8DqVkkWGlQ5CWqnxci1NAXLeRKlSO9M2E/UZVTxdBg==} - '@polkadot-api/known-chains@0.11.3': resolution: {integrity: sha512-ObvFOgVchOEGX3VR4g0LrHOkdltl9kF5xTgVLErfs3dUl9OhG0MaFs8vB2n9GBdp616T3tOFAgKYZNSFtBomyg==} + '@polkadot-api/known-chains@0.11.4': + resolution: {integrity: sha512-srEr1NIknt3dewnthffBfjNwfRh4nxQOrWhy3//ltNU8HH8Jh2RLZ6hTOOfUL5UiOzYod0IHzfprK1/g9IoyPg==} + '@polkadot-api/logs-provider@0.2.0': resolution: {integrity: sha512-BH9YdxZu+ZBPPAUwGrvqHPn1hQStL2Im3MmTwYkwXOWW2HlGHULcF65QKvyK+T6/mj2vDvl3MgivnGkUw4pVxg==} '@polkadot-api/merkleize-metadata@1.2.2': resolution: {integrity: sha512-3u1ycUlezLeK80gXkxIPF1bgr/ZZ8XxAPZacJh+3uSjOhB9D/ShenM7uifihxXDYkGD/mS1Cr9DCjPZ7X1ZLKQ==} - '@polkadot-api/metadata-builders@0.14.1': - resolution: {integrity: sha512-kHgRagFfPnJuuSG2Fm3cHvqPwFkUZ1etkwwGboFCPatOxbovNgf7RGvKcCDwtL1LLeS0sADYw4nUrNyDGiJBCw==} - '@polkadot-api/metadata-builders@0.14.2': resolution: {integrity: sha512-nhsFfti0M5tE0LR8++0wHqbP54I/QSFXP/uF5I82MYVSKY0NqIDkIFvr27oLo/ltF+o3vcN44ZJQqvi1k6l9mA==} + '@polkadot-api/metadata-builders@0.14.3': + resolution: {integrity: sha512-m7CACsiqHzgVEh5WBZGkTV8AQ3CBQKR1YpPQMnlsJfCr/IkgKU0UyWM6WxCmBiReLFVkOfXMtGlpN8+GxpHmww==} + '@polkadot-api/metadata-builders@0.3.2': resolution: {integrity: sha512-TKpfoT6vTb+513KDzMBTfCb/ORdgRnsS3TDFpOhAhZ08ikvK+hjHMt5plPiAX/OWkm1Wc9I3+K6W0hX5Ab7MVg==} - '@polkadot-api/metadata-compatibility@0.6.1': - resolution: {integrity: sha512-9b5GAukwAkedLh3aw3qWyGAeXKYA493CV0Beqjs1UfjvKSo2BjNXgm3xdMUfi+rgcTE3pkJEu2NMxcmrHmmnIA==} - '@polkadot-api/metadata-compatibility@0.6.2': resolution: {integrity: sha512-ht9rVELif1uwkNi5pVZCwfdc+fKJXfvBVmyBLaiuyoZYEokD7Bxu/DVsa/uTRsYkEhgd/FxBuDG32CM/mcphUw==} - '@polkadot-api/observable-client@0.18.4': - resolution: {integrity: sha512-5cp/tzoe3WOwABsIfZNQC2e2vlXMzZ9dSCk6q9lfJj3SN/RRcu4Wpk5G8PxtQj7bZ4OAHnAMdwR6juCeQdDZMw==} - peerDependencies: - rxjs: '>=7.8.0' + '@polkadot-api/metadata-compatibility@0.6.3': + resolution: {integrity: sha512-/Y0uF8nDk60ijydp8Bd37YexPFdB8hBXJWwEgOJHsVlhiny8sVKXiMg+UkJ9BiEk2z+yMbZRCKmhNpTpizo7aw==} '@polkadot-api/observable-client@0.18.5': resolution: {integrity: sha512-MXQzh1pzySQcyj5hwImfxo1fGOKXCFVyAjaG+9KZZVZDyS+V5yoNiYReiXaI68k50GpbvW1b9ZoUiANKwx8PRA==} peerDependencies: rxjs: '>=7.8.0' + '@polkadot-api/observable-client@0.18.6': + resolution: {integrity: sha512-vCSi/kGNt6gl4J6lqcJM9C0tU/toZZ032dT6xrDLiRZ5bhJRbVts2PIWOksit5lEXc9jnqWrS+e6YXCXQMFOsw==} + peerDependencies: + rxjs: '>=7.8.0' + '@polkadot-api/observable-client@0.3.2': resolution: {integrity: sha512-HGgqWgEutVyOBXoGOPp4+IAq6CNdK/3MfQJmhCJb8YaJiaK4W6aRGrdQuQSTPHfERHCARt9BrOmEvTXAT257Ug==} peerDependencies: @@ -842,28 +842,28 @@ packages: '@polkadot-api/signers-common@0.2.2': resolution: {integrity: sha512-zuge7psdtxBYK7qIgCb1mcy7YfjVdagOUhrdXAfUrfQ3aH1/PNCVke20v9rOHnrsAzaxb/+MdTBQGZ6MY7hHUA==} - '@polkadot-api/sm-provider@0.3.1': - resolution: {integrity: sha512-SyuE8t3P95/Ii0JQFemAZzwAj/LBCkVyD/u2rjSvz94ayoCHO3MP0ds06kNyjOBerike9R7fY/nWE1EmEkgxCA==} - peerDependencies: - '@polkadot-api/smoldot': '>=0.3' - '@polkadot-api/sm-provider@0.3.2': resolution: {integrity: sha512-CkUn+CDB35bODG/snT2beMjzrnotn14pdI4TsxBeo9AMtVxLwAQW295nRfZs1ylGoKz1nqcb7ZVpxidee1fKlQ==} peerDependencies: '@polkadot-api/smoldot': '>=0.3' - '@polkadot-api/smoldot@0.4.1': - resolution: {integrity: sha512-RHlxPVS+x/BcJzyslMiBtjPIqnAwjpZ4dnYYAG9mzNeY94BO9JUpFZH9XwReZN62am9Nurul2JlLclB4jVqCaw==} + '@polkadot-api/sm-provider@0.3.4': + resolution: {integrity: sha512-flE9fJL0ESxlKBHOXgWuHgKQJMP059z0BlZP6PRya5yDpUMC5KrtXbvzdaF+o8rDAMxqL+nDf38+P9h/BrmK7A==} + peerDependencies: + '@polkadot-api/smoldot': '>=0.3' '@polkadot-api/smoldot@0.4.2': resolution: {integrity: sha512-6gNBSXwdQYfPvLyPq5L/bXPTZiBW9ipvwjhThcOycRVAQs1GqrZ8WyNyuiBI4MIwL8OT4vfHMdg1yv9+4dKdYQ==} - '@polkadot-api/substrate-bindings@0.20.1': - resolution: {integrity: sha512-AU7uUqSFt6nH8wrD7DJZkmrFs1QjLeA/Z4Qmf03ExUqsKDQ893pPjbQU2qAaPvWzDmaJotT8B8DENRqEUt8ruw==} + '@polkadot-api/smoldot@0.4.3': + resolution: {integrity: sha512-M2LpGG0XOeHDIGre2aUr5W3kCz9MjQ8kGlQa1h7aS2XJwlPrjbJe/MCo12FgZKb1TD/ACpqQi9TgO1B2+8F98w==} '@polkadot-api/substrate-bindings@0.20.2': resolution: {integrity: sha512-js5UTREoI+FlrPRXMhtKimVWmOqwfNFBnhyshsdloSZHNx/Hulg2RQZNvrVTscyZTf8LyxlGJaH5dsitOUoFKw==} + '@polkadot-api/substrate-bindings@0.20.3': + resolution: {integrity: sha512-9iqC71fx1ee9ld1NZV8PFime5vryi0kt1bKCSlvNgO6dqMc06sMZuZ8WPjOzWLCHiKHLuphdMs3rVBBaeCP3yg==} + '@polkadot-api/substrate-bindings@0.6.0': resolution: {integrity: sha512-lGuhE74NA1/PqdN7fKFdE5C1gNYX357j1tWzdlPXI0kQ7h3kN0zfxNOpPUN7dIrPcOFZ6C0tRRVrBylXkI6xPw==} @@ -882,13 +882,13 @@ packages: '@polkadot-api/wasm-executor@0.2.3': resolution: {integrity: sha512-B2h1o+Qlo9idpASaHvMSoViB2I5ko5OAfwfhYF8LQDkTADK0B+SeStzNj1Qn+FG34wqTuv7HzBCdjaUgzYINJQ==} - '@polkadot-api/ws-middleware@0.3.2': - resolution: {integrity: sha512-x3WuA59NrcIbhfFw+LRnlDNRdMRdg9Wz8OCK6HUjjwFfL/O+yNtf/pTGuINuOigZzmBj7hHixPaVw9VJogCN6Q==} + '@polkadot-api/ws-middleware@0.3.3': + resolution: {integrity: sha512-MIoV/3jBMk1z5mbf5cSwzRuDis7vhePZHUoJjlqd0zBzUGoABgDALA+gBSHmDU3U08T7o7TbEeJFSBJEh4lp7g==} peerDependencies: rxjs: '>=7.8.0' - '@polkadot-api/ws-middleware@0.3.3': - resolution: {integrity: sha512-MIoV/3jBMk1z5mbf5cSwzRuDis7vhePZHUoJjlqd0zBzUGoABgDALA+gBSHmDU3U08T7o7TbEeJFSBJEh4lp7g==} + '@polkadot-api/ws-middleware@0.3.4': + resolution: {integrity: sha512-0Op3ifcYV2kp4X4vdHQSXNNTXS9FSyIaM3f7NXBtnA2nnZaRB+5JDU8SI1LRlxeYjnLWlQW3sU61avRt0qgJrA==} peerDependencies: rxjs: '>=7.8.0' @@ -1589,139 +1589,264 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.61.0': + resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.60.2': resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.61.0': + resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.60.2': resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.61.0': + resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.60.2': resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.61.0': + resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.60.2': resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.61.0': + resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.60.2': resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.61.0': + resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] libc: [glibc] + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] libc: [musl] + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-arm64-gnu@4.61.0': + resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] libc: [musl] + '@rollup/rollup-linux-arm64-musl@4.61.0': + resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-loong64-gnu@4.61.0': + resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] libc: [musl] + '@rollup/rollup-linux-loong64-musl@4.61.0': + resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] libc: [musl] + '@rollup/rollup-linux-ppc64-musl@4.61.0': + resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] libc: [musl] + '@rollup/rollup-linux-riscv64-musl@4.61.0': + resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] libc: [glibc] + '@rollup/rollup-linux-s390x-gnu@4.61.0': + resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-x64-gnu@4.61.0': + resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] libc: [musl] + '@rollup/rollup-linux-x64-musl@4.61.0': + resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + cpu: [x64] + os: [linux] + '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} cpu: [x64] os: [openbsd] + '@rollup/rollup-openbsd-x64@4.61.0': + resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + cpu: [x64] + os: [openbsd] + '@rollup/rollup-openharmony-arm64@4.60.2': resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.61.0': + resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.60.2': resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.61.0': + resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.60.2': resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.61.0': + resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.60.2': resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.61.0': + resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.60.2': resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.61.0': + resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + cpu: [x64] + os: [win32] + '@rx-state/core@0.1.4': resolution: {integrity: sha512-Z+3hjU2xh1HisLxt+W5hlYX/eGSDaXXP+ns82gq/PLZpkXLu0uwcNUh9RLY3Clq4zT+hSsA3vcpIGt6+UAb8rQ==} peerDependencies: @@ -1877,6 +2002,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1886,6 +2014,9 @@ packages: '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2783,6 +2914,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.61.0: + resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -2818,12 +2954,12 @@ packages: smoldot@2.0.26: resolution: {integrity: sha512-F+qYmH4z2s2FK+CxGj8moYcd1ekSIKH8ywkdqlOz88Dat35iB1DIYL11aILN46YSGMzQW/lbJNS307zBSDN5Ig==} - smoldot@3.1.0: - resolution: {integrity: sha512-LZcex0BnfY1ORmuj0rKEF1aw7/gdFq6e5bBsLWd6x/fTt63mpAORhxf6fTsQcce0yLtbzkll8J+YKwhJLYATdw==} - smoldot@3.1.1: resolution: {integrity: sha512-aPc/0qCUB6RWwpGhLmDi7odeQTrLZfsDpZjxYQZRqqcGUituI2O26QSl3fFREJOczl4/R8duAmH2Mu4D7HoPww==} + smoldot@3.1.4: + resolution: {integrity: sha512-RmqzA09GuluKwa5spSbAQ0t+ph2hY0BViKt/VBHBmLO/kbLk8fsQ+5NqQnxzpxJKYoYsROBscsumOxYUxW4hmQ==} + sort-keys@5.1.0: resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==} engines: {node: '>=12'} @@ -2962,6 +3098,9 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -3399,22 +3538,22 @@ snapshots: dependencies: playwright: 1.59.1 - '@polkadot-api/cli@0.20.4(esbuild@0.28.0)': + '@polkadot-api/cli@0.21.0(esbuild@0.28.0)': dependencies: '@commander-js/extra-typings': 14.0.0(commander@14.0.3) - '@polkadot-api/codegen': 0.22.3 - '@polkadot-api/ink-contracts': 0.6.1 + '@polkadot-api/codegen': 0.22.4 + '@polkadot-api/ink-contracts': 0.6.2 '@polkadot-api/json-rpc-provider': 0.2.0 - '@polkadot-api/known-chains': 0.11.2 - '@polkadot-api/metadata-compatibility': 0.6.1 - '@polkadot-api/observable-client': 0.18.4(rxjs@7.8.2) - '@polkadot-api/sm-provider': 0.3.1(@polkadot-api/smoldot@0.4.1) - '@polkadot-api/smoldot': 0.4.1 - '@polkadot-api/substrate-bindings': 0.20.1 + '@polkadot-api/known-chains': 0.11.3 + '@polkadot-api/metadata-compatibility': 0.6.2 + '@polkadot-api/observable-client': 0.18.5(rxjs@7.8.2) + '@polkadot-api/sm-provider': 0.3.2(@polkadot-api/smoldot@0.4.2) + '@polkadot-api/smoldot': 0.4.2 + '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/substrate-client': 0.7.0 '@polkadot-api/utils': 0.4.0 '@polkadot-api/wasm-executor': 0.2.3 - '@polkadot-api/ws-middleware': 0.3.2(rxjs@7.8.2) + '@polkadot-api/ws-middleware': 0.3.3(rxjs@7.8.2) '@polkadot-api/ws-provider': 0.9.0(rxjs@7.8.2) '@types/node': 25.6.0 commander: 14.0.3 @@ -3434,31 +3573,31 @@ snapshots: - supports-color - utf-8-validate - '@polkadot-api/cli@0.21.0(esbuild@0.28.0)': + '@polkadot-api/cli@0.21.3(esbuild@0.28.0)': dependencies: '@commander-js/extra-typings': 14.0.0(commander@14.0.3) - '@polkadot-api/codegen': 0.22.4 - '@polkadot-api/ink-contracts': 0.6.2 + '@polkadot-api/codegen': 0.22.5 + '@polkadot-api/ink-contracts': 0.6.3 '@polkadot-api/json-rpc-provider': 0.2.0 - '@polkadot-api/known-chains': 0.11.3 - '@polkadot-api/metadata-compatibility': 0.6.2 - '@polkadot-api/observable-client': 0.18.5(rxjs@7.8.2) - '@polkadot-api/sm-provider': 0.3.2(@polkadot-api/smoldot@0.4.2) - '@polkadot-api/smoldot': 0.4.2 - '@polkadot-api/substrate-bindings': 0.20.2 + '@polkadot-api/known-chains': 0.11.4 + '@polkadot-api/metadata-compatibility': 0.6.3 + '@polkadot-api/observable-client': 0.18.6(rxjs@7.8.2) + '@polkadot-api/sm-provider': 0.3.4(@polkadot-api/smoldot@0.4.3) + '@polkadot-api/smoldot': 0.4.3 + '@polkadot-api/substrate-bindings': 0.20.3 '@polkadot-api/substrate-client': 0.7.0 '@polkadot-api/utils': 0.4.0 '@polkadot-api/wasm-executor': 0.2.3 - '@polkadot-api/ws-middleware': 0.3.3(rxjs@7.8.2) + '@polkadot-api/ws-middleware': 0.3.4(rxjs@7.8.2) '@polkadot-api/ws-provider': 0.9.0(rxjs@7.8.2) - '@types/node': 25.6.0 + '@types/node': 25.9.1 commander: 14.0.3 execa: 9.6.1 fs.promises.exists: 1.1.4 ora: 9.4.0 read-pkg: 10.1.0 - rollup: 4.60.2 - rollup-plugin-esbuild: 6.2.1(esbuild@0.28.0)(rollup@4.60.2) + rollup: 4.61.0 + rollup-plugin-esbuild: 6.2.1(esbuild@0.28.0)(rollup@4.61.0) rxjs: 7.8.2 tsc-prog: 2.3.0(typescript@6.0.3) typescript: 6.0.3 @@ -3469,14 +3608,6 @@ snapshots: - supports-color - utf-8-validate - '@polkadot-api/codegen@0.22.3': - dependencies: - '@polkadot-api/ink-contracts': 0.6.1 - '@polkadot-api/metadata-builders': 0.14.1 - '@polkadot-api/metadata-compatibility': 0.6.1 - '@polkadot-api/substrate-bindings': 0.20.1 - '@polkadot-api/utils': 0.4.0 - '@polkadot-api/codegen@0.22.4': dependencies: '@polkadot-api/ink-contracts': 0.6.2 @@ -3485,10 +3616,12 @@ snapshots: '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/utils': 0.4.0 - '@polkadot-api/ink-contracts@0.6.1': + '@polkadot-api/codegen@0.22.5': dependencies: - '@polkadot-api/metadata-builders': 0.14.1 - '@polkadot-api/substrate-bindings': 0.20.1 + '@polkadot-api/ink-contracts': 0.6.3 + '@polkadot-api/metadata-builders': 0.14.3 + '@polkadot-api/metadata-compatibility': 0.6.3 + '@polkadot-api/substrate-bindings': 0.20.3 '@polkadot-api/utils': 0.4.0 '@polkadot-api/ink-contracts@0.6.2': @@ -3497,6 +3630,12 @@ snapshots: '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/utils': 0.4.0 + '@polkadot-api/ink-contracts@0.6.3': + dependencies: + '@polkadot-api/metadata-builders': 0.14.3 + '@polkadot-api/substrate-bindings': 0.20.3 + '@polkadot-api/utils': 0.4.0 + '@polkadot-api/json-rpc-provider-proxy@0.1.0': optional: true @@ -3507,10 +3646,10 @@ snapshots: '@polkadot-api/json-rpc-provider@0.2.0': {} - '@polkadot-api/known-chains@0.11.2': {} - '@polkadot-api/known-chains@0.11.3': {} + '@polkadot-api/known-chains@0.11.4': {} + '@polkadot-api/logs-provider@0.2.0': dependencies: '@polkadot-api/json-rpc-provider': 0.2.0 @@ -3521,14 +3660,14 @@ snapshots: '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/utils': 0.4.0 - '@polkadot-api/metadata-builders@0.14.1': + '@polkadot-api/metadata-builders@0.14.2': dependencies: - '@polkadot-api/substrate-bindings': 0.20.1 + '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/utils': 0.4.0 - '@polkadot-api/metadata-builders@0.14.2': + '@polkadot-api/metadata-builders@0.14.3': dependencies: - '@polkadot-api/substrate-bindings': 0.20.2 + '@polkadot-api/substrate-bindings': 0.20.3 '@polkadot-api/utils': 0.4.0 '@polkadot-api/metadata-builders@0.3.2': @@ -3537,23 +3676,15 @@ snapshots: '@polkadot-api/utils': 0.1.0 optional: true - '@polkadot-api/metadata-compatibility@0.6.1': - dependencies: - '@polkadot-api/metadata-builders': 0.14.1 - '@polkadot-api/substrate-bindings': 0.20.1 - '@polkadot-api/metadata-compatibility@0.6.2': dependencies: '@polkadot-api/metadata-builders': 0.14.2 '@polkadot-api/substrate-bindings': 0.20.2 - '@polkadot-api/observable-client@0.18.4(rxjs@7.8.2)': + '@polkadot-api/metadata-compatibility@0.6.3': dependencies: - '@polkadot-api/metadata-builders': 0.14.1 - '@polkadot-api/substrate-bindings': 0.20.1 - '@polkadot-api/substrate-client': 0.7.0 - '@polkadot-api/utils': 0.4.0 - rxjs: 7.8.2 + '@polkadot-api/metadata-builders': 0.14.3 + '@polkadot-api/substrate-bindings': 0.20.3 '@polkadot-api/observable-client@0.18.5(rxjs@7.8.2)': dependencies: @@ -3563,6 +3694,14 @@ snapshots: '@polkadot-api/utils': 0.4.0 rxjs: 7.8.2 + '@polkadot-api/observable-client@0.18.6(rxjs@7.8.2)': + dependencies: + '@polkadot-api/metadata-builders': 0.14.3 + '@polkadot-api/substrate-bindings': 0.20.3 + '@polkadot-api/substrate-client': 0.7.0 + '@polkadot-api/utils': 0.4.0 + rxjs: 7.8.2 + '@polkadot-api/observable-client@0.3.2(@polkadot-api/substrate-client@0.1.4)(rxjs@7.8.2)': dependencies: '@polkadot-api/metadata-builders': 0.3.2 @@ -3602,42 +3741,42 @@ snapshots: '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/utils': 0.4.0 - '@polkadot-api/sm-provider@0.3.1(@polkadot-api/smoldot@0.4.1)': + '@polkadot-api/sm-provider@0.3.2(@polkadot-api/smoldot@0.4.2)': dependencies: '@polkadot-api/json-rpc-provider': 0.2.0 '@polkadot-api/json-rpc-provider-proxy': 0.4.0 - '@polkadot-api/smoldot': 0.4.1 + '@polkadot-api/smoldot': 0.4.2 - '@polkadot-api/sm-provider@0.3.2(@polkadot-api/smoldot@0.4.2)': + '@polkadot-api/sm-provider@0.3.4(@polkadot-api/smoldot@0.4.3)': dependencies: '@polkadot-api/json-rpc-provider': 0.2.0 '@polkadot-api/json-rpc-provider-proxy': 0.4.0 - '@polkadot-api/smoldot': 0.4.2 + '@polkadot-api/smoldot': 0.4.3 - '@polkadot-api/smoldot@0.4.1': + '@polkadot-api/smoldot@0.4.2': dependencies: '@types/node': 25.6.0 - smoldot: 3.1.0 + smoldot: 3.1.1 transitivePeerDependencies: - bufferutil - utf-8-validate - '@polkadot-api/smoldot@0.4.2': + '@polkadot-api/smoldot@0.4.3': dependencies: - '@types/node': 25.6.0 - smoldot: 3.1.1 + '@types/node': 25.9.1 + smoldot: 3.1.4 transitivePeerDependencies: - bufferutil - utf-8-validate - '@polkadot-api/substrate-bindings@0.20.1': + '@polkadot-api/substrate-bindings@0.20.2': dependencies: '@noble/hashes': 2.2.0 '@polkadot-api/utils': 0.4.0 '@scure/base': 2.2.0 scale-ts: 1.6.1 - '@polkadot-api/substrate-bindings@0.20.2': + '@polkadot-api/substrate-bindings@0.20.3': dependencies: '@noble/hashes': 2.2.0 '@polkadot-api/utils': 0.4.0 @@ -3671,21 +3810,21 @@ snapshots: '@polkadot-api/wasm-executor@0.2.3': {} - '@polkadot-api/ws-middleware@0.3.2(rxjs@7.8.2)': + '@polkadot-api/ws-middleware@0.3.3(rxjs@7.8.2)': dependencies: '@polkadot-api/json-rpc-provider': 0.2.0 '@polkadot-api/json-rpc-provider-proxy': 0.4.0 '@polkadot-api/raw-client': 0.3.0 - '@polkadot-api/substrate-bindings': 0.20.1 + '@polkadot-api/substrate-bindings': 0.20.2 '@polkadot-api/utils': 0.4.0 rxjs: 7.8.2 - '@polkadot-api/ws-middleware@0.3.3(rxjs@7.8.2)': + '@polkadot-api/ws-middleware@0.3.4(rxjs@7.8.2)': dependencies: '@polkadot-api/json-rpc-provider': 0.2.0 '@polkadot-api/json-rpc-provider-proxy': 0.4.0 '@polkadot-api/raw-client': 0.3.0 - '@polkadot-api/substrate-bindings': 0.20.2 + '@polkadot-api/substrate-bindings': 0.20.3 '@polkadot-api/utils': 0.4.0 rxjs: 7.8.2 @@ -4488,78 +4627,153 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.60.2': optional: true + '@rollup/rollup-android-arm-eabi@4.61.0': + optional: true + '@rollup/rollup-android-arm64@4.60.2': optional: true + '@rollup/rollup-android-arm64@4.61.0': + optional: true + '@rollup/rollup-darwin-arm64@4.60.2': optional: true + '@rollup/rollup-darwin-arm64@4.61.0': + optional: true + '@rollup/rollup-darwin-x64@4.60.2': optional: true + '@rollup/rollup-darwin-x64@4.61.0': + optional: true + '@rollup/rollup-freebsd-arm64@4.60.2': optional: true + '@rollup/rollup-freebsd-arm64@4.61.0': + optional: true + '@rollup/rollup-freebsd-x64@4.60.2': optional: true + '@rollup/rollup-freebsd-x64@4.61.0': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.60.2': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-arm64-gnu@4.61.0': + optional: true + '@rollup/rollup-linux-arm64-musl@4.60.2': optional: true + '@rollup/rollup-linux-arm64-musl@4.61.0': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-loong64-gnu@4.61.0': + optional: true + '@rollup/rollup-linux-loong64-musl@4.60.2': optional: true + '@rollup/rollup-linux-loong64-musl@4.61.0': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + optional: true + '@rollup/rollup-linux-ppc64-musl@4.60.2': optional: true + '@rollup/rollup-linux-ppc64-musl@4.61.0': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.60.2': optional: true + '@rollup/rollup-linux-riscv64-musl@4.61.0': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.60.2': optional: true + '@rollup/rollup-linux-s390x-gnu@4.61.0': + optional: true + '@rollup/rollup-linux-x64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-x64-gnu@4.61.0': + optional: true + '@rollup/rollup-linux-x64-musl@4.60.2': optional: true + '@rollup/rollup-linux-x64-musl@4.61.0': + optional: true + '@rollup/rollup-openbsd-x64@4.60.2': optional: true + '@rollup/rollup-openbsd-x64@4.61.0': + optional: true + '@rollup/rollup-openharmony-arm64@4.60.2': optional: true + '@rollup/rollup-openharmony-arm64@4.61.0': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.60.2': optional: true + '@rollup/rollup-win32-arm64-msvc@4.61.0': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.60.2': optional: true + '@rollup/rollup-win32-ia32-msvc@4.61.0': + optional: true + '@rollup/rollup-win32-x64-gnu@4.60.2': optional: true + '@rollup/rollup-win32-x64-gnu@4.61.0': + optional: true + '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true + '@rollup/rollup-win32-x64-msvc@4.61.0': + optional: true + '@rx-state/core@0.1.4(rxjs@7.8.2)': dependencies: rxjs: 7.8.2 @@ -4676,12 +4890,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 - '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))': + '@tailwindcss/vite@4.2.4(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1))': dependencies: '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 tailwindcss: 4.2.4 - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) '@tybys/wasm-util@0.10.2': dependencies: @@ -4701,6 +4915,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@22.19.17': @@ -4711,6 +4927,10 @@ snapshots: dependencies: undici-types: 7.19.2 + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + '@types/normalize-package-data@2.4.4': {} '@types/react-dom@19.2.3(@types/react@19.2.14)': @@ -4812,10 +5032,10 @@ snapshots: '@typescript-eslint/types': 8.59.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))': + '@vitejs/plugin-react@6.0.1(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) '@vitest/expect@4.1.5': dependencies: @@ -4826,13 +5046,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1))': + '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) '@vitest/pretty-format@4.1.5': dependencies: @@ -5609,6 +5829,17 @@ snapshots: transitivePeerDependencies: - supports-color + rollup-plugin-esbuild@6.2.1(esbuild@0.28.0)(rollup@4.61.0): + dependencies: + debug: 4.4.3 + es-module-lexer: 1.7.0 + esbuild: 0.28.0 + get-tsconfig: 4.14.0 + rollup: 4.61.0 + unplugin-utils: 0.2.5 + transitivePeerDependencies: + - supports-color + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 @@ -5640,6 +5871,37 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 + rollup@4.61.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.0 + '@rollup/rollup-android-arm64': 4.61.0 + '@rollup/rollup-darwin-arm64': 4.61.0 + '@rollup/rollup-darwin-x64': 4.61.0 + '@rollup/rollup-freebsd-arm64': 4.61.0 + '@rollup/rollup-freebsd-x64': 4.61.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 + '@rollup/rollup-linux-arm-musleabihf': 4.61.0 + '@rollup/rollup-linux-arm64-gnu': 4.61.0 + '@rollup/rollup-linux-arm64-musl': 4.61.0 + '@rollup/rollup-linux-loong64-gnu': 4.61.0 + '@rollup/rollup-linux-loong64-musl': 4.61.0 + '@rollup/rollup-linux-ppc64-gnu': 4.61.0 + '@rollup/rollup-linux-ppc64-musl': 4.61.0 + '@rollup/rollup-linux-riscv64-gnu': 4.61.0 + '@rollup/rollup-linux-riscv64-musl': 4.61.0 + '@rollup/rollup-linux-s390x-gnu': 4.61.0 + '@rollup/rollup-linux-x64-gnu': 4.61.0 + '@rollup/rollup-linux-x64-musl': 4.61.0 + '@rollup/rollup-openbsd-x64': 4.61.0 + '@rollup/rollup-openharmony-arm64': 4.61.0 + '@rollup/rollup-win32-arm64-msvc': 4.61.0 + '@rollup/rollup-win32-ia32-msvc': 4.61.0 + '@rollup/rollup-win32-x64-gnu': 4.61.0 + '@rollup/rollup-win32-x64-msvc': 4.61.0 + fsevents: 2.3.3 + rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -5670,14 +5932,14 @@ snapshots: - utf-8-validate optional: true - smoldot@3.1.0: + smoldot@3.1.1: dependencies: ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate - smoldot@3.1.1: + smoldot@3.1.4: dependencies: ws: 8.20.0 transitivePeerDependencies: @@ -5798,6 +6060,8 @@ snapshots: undici-types@7.19.2: {} + undici-types@7.24.6: {} + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} @@ -5843,7 +6107,7 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1): + vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -5851,15 +6115,15 @@ snapshots: rolldown: 1.0.0-rc.17 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 25.9.1 esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.6.1 - vitest@4.1.5(@types/node@25.6.0)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)): + vitest@4.1.5(@types/node@25.9.1)(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)) + '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -5876,10 +6140,10 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1) + vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.6.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.6.0 + '@types/node': 25.9.1 transitivePeerDependencies: - msw diff --git a/user-interfaces/pnpm-workspace.yaml b/user-interfaces/pnpm-workspace.yaml index ff0e61c8..1cc7f57d 100644 --- a/user-interfaces/pnpm-workspace.yaml +++ b/user-interfaces/pnpm-workspace.yaml @@ -1,9 +1,9 @@ packages: - - "shared/*" - # The magic-named @polkadot-api/descriptors package lives nested inside - # shared/papi/.papi/. PAPI tooling regenerates it there; we register it as - # a workspace package so consumers can depend on it via `workspace:*`. - - "shared/papi/.papi/descriptors" - - "provider" - - "console-ui" - - "drive-ui" + - shared/* + - shared/papi/.papi/descriptors + - provider + - console-ui + - drive-ui + +allowBuilds: + esbuild: true diff --git a/user-interfaces/provider/e2e/integration/displays.spec.ts b/user-interfaces/provider/e2e/integration/displays.spec.ts index 5ca2f60e..8151ae6d 100644 --- a/user-interfaces/provider/e2e/integration/displays.spec.ts +++ b/user-interfaces/provider/e2e/integration/displays.spec.ts @@ -45,9 +45,10 @@ test("Buckets page renders the buckets table", async ({ localPage }) => { ).toBeVisible({ timeout: 30_000 }); }); -test("Agreements page renders the agreements table", async ({ localPage }) => { - await localPage.getByTestId("nav-agreements").click(); - await expect( - localPage.locator('[data-testid="agreements-table"], :text("No active agreements")'), - ).toBeVisible({ timeout: 30_000 }); -}); +// test("Agreements page renders the agreements table", async ({ localPage }) => { +// await localPage.getByTestId("nav-agreements").click(); +// await expect( +// localPage.locator('[data-testid="agreements-table"], :text("No active agreements")'), +// ).toBeVisible({ timeout: 30_000 }); +// }); + diff --git a/user-interfaces/provider/src/App.tsx b/user-interfaces/provider/src/App.tsx index 193d7cb1..045032d0 100644 --- a/user-interfaces/provider/src/App.tsx +++ b/user-interfaces/provider/src/App.tsx @@ -3,7 +3,7 @@ import { Routes, Route } from 'react-router-dom' import { Header } from '@/components/Header' import { Overview } from '@/pages/Overview' import { Registration } from '@/pages/Registration' -import { Agreements } from '@/pages/Agreements' +// import { Agreements } from '@/pages/Agreements' import { Buckets } from '@/pages/Buckets' import { Checkpoints } from '@/pages/Checkpoints' import { Challenges } from '@/pages/Challenges' @@ -39,7 +39,7 @@ function App() { } /> } /> - } /> + {/* } /> */} } /> } /> } /> diff --git a/user-interfaces/provider/src/components/Header.tsx b/user-interfaces/provider/src/components/Header.tsx index a205bb65..6d49f062 100644 --- a/user-interfaces/provider/src/components/Header.tsx +++ b/user-interfaces/provider/src/components/Header.tsx @@ -43,7 +43,7 @@ import { formatAddress } from '@/utils/format' const navItems = [ { path: '/', label: 'Overview', icon: Server }, { path: '/registration', label: 'Registration', icon: Settings }, - { path: '/agreements', label: 'Agreements', icon: FileText }, + // { path: '/agreements', label: 'Agreements', icon: FileText }, { path: '/buckets', label: 'Buckets', icon: Database }, { path: '/checkpoints', label: 'Checkpoints', icon: CheckCircle }, { path: '/challenges', label: 'Challenges', icon: Shield }, diff --git a/user-interfaces/provider/src/lib/chain-client.ts b/user-interfaces/provider/src/lib/chain-client.ts index 1bb43a79..0fc6ae38 100644 --- a/user-interfaces/provider/src/lib/chain-client.ts +++ b/user-interfaces/provider/src/lib/chain-client.ts @@ -101,9 +101,8 @@ export async function getChainProperties(): Promise<{ } catch { /* use default */ } try { - // expectedBlockTime = 2 * MinimumPeriod (Aura convention) - const period = await api.constants.Timestamp.MinimumPeriod() - blockTimeMs = Number(period) * 2 + const period = await api.constants.Aura.SlotDuration() + blockTimeMs = Number(period) } catch { /* use default */ } } @@ -370,22 +369,23 @@ export async function getProviderAgreements(address: string): Promise { - const entries = await requireApi().query.StorageProvider.AgreementRequests.getEntries() - const out: OnChainAgreementRequest[] = [] - for (const { keyArgs, value } of entries) { - const [bucketIdRaw, providerAddr] = keyArgs - if (providerAddr !== address) continue - out.push({ - bucketId: Number(bucketIdRaw), - requester: value.requester, - maxBytes: value.max_bytes, - paymentLocked: value.payment_locked, - duration: value.duration, - expiresAt: value.expires_at, - }) - } - return out +export async function getAgreementRequests(_address: string): Promise { + // const entries = await requireApi().query.StorageProvider.AgreementRequests.getEntries() + // const out: OnChainAgreementRequest[] = [] + // for (const { keyArgs, value } of entries) { + // const [bucketIdRaw, providerAddr] = keyArgs + // if (providerAddr !== address) continue + // out.push({ + // bucketId: Number(bucketIdRaw), + // requester: value.requester, + // maxBytes: value.max_bytes, + // paymentLocked: value.payment_locked, + // duration: value.duration, + // expiresAt: value.expires_at, + // }) + // } + // return out + return []; } export async function getProviderCheckpoints(address: string): Promise { diff --git a/user-interfaces/shared/papi/.papi/descriptors/package.json b/user-interfaces/shared/papi/.papi/descriptors/package.json index b809d4b8..19497ba7 100644 --- a/user-interfaces/shared/papi/.papi/descriptors/package.json +++ b/user-interfaces/shared/papi/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.14101500820978873743", + "version": "0.1.0-autogenerated.449225876343641783", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/user-interfaces/shared/papi/.papi/metadata/parachain.scale b/user-interfaces/shared/papi/.papi/metadata/parachain.scale index fe6d32fea7df4013119c120b1faab762c64b013f..4d5747827fc1bebec04e31eacd51c816cc1b9f28 100644 GIT binary patch delta 10363 zcmcIK3v?CLwP&B1xk*fbgnUSV$OICSK)!@efFzKR0D&Zgd;|(H8Sb4VSMDdd_XdcS z7t=~B3sHE8TUt`Vii!$o5yq$}tU_B*+Qxz{Dn99g*1n2QDim4b^X)VD-XsXMeQ&L| z)=g&S%s%_zl9|_bgxfa>9BiS8-qx-7LTqwb(b#!kB)&+ zsWCp?O=yxJ2g+I+P`wC-9_)cc2|o-SFM`Ek;P)6u2t_x zN$*-*UV^EeQ(RG&sk^Lh8?CUstLZj0HZ`d3=H^V3Q8Taj2#tG+t9qVc^m4(XBMeqqjMT58>2P^3m^gi!%cgm3{aebmYHga;?(57ZiL9b+ zx9Zkav_+Rqb+_rXS(=sB&!MbRukLGf+Fgx7E_*{Ec_q#ag{zQj7Ak0x}ADj zV|vpd0h-*todCP^tdC6szEy%GxZ4WwgCHOOx%^BDNJysq6bMdS_^(LN)Eg4SfUc`< zSB2(M^|f}dPbiIfD)7A_umGkQDMO)Ff$4ZY7V7cW(a;gNH@3&XJb>v2y?8NH?mvIV z{ySkRhcZ-u3dvy}-P`7NdG$(+PJ(8zjFV3OeXJ4&*^ZOO8 zQJmWRn@?2)j(Hp6aQt-`8o@?Gj42HnlCdxX(z-HA9B%8Xst)H$-NPWCkXp%7>7K@w zoj&~-j4sg}8l4LU8LKrqD2*L1pWTrUH&2T(&rTXdbWEL0NN(1PLkxtmlA#Dva6<~DLpmNvfw6RXHwDz1ObKS|R-0FwF{3tUR~TmzWxiIK z(aWWQmkV{PRa-RxLKLRcpER4Ojvzp~EjDj+u`L;5u{;^Z!xaC0NDAcg=@RA+AE;@aQ#A@$`6@ z2#X&}g)CCU>{Lhq4a-x(HK0v+CKZPB#pa?V1Y{CboCeWl%Opq^3(uc4Whyz0NQbU9 zH%KtL(&?$7TLx)jL>Mx6vnD}CrS4hf(5uBGW6K161+2l3Z(kae(#k|?9EPPs{IqZ4 zZqvw22G(2Yb~|XVy1?)&W~Yl3LL75S%!y0WA%(Zo=EC@$*p>r#;4|qkbYvt%x-_TW zAwfYv!35zQ`9(#T6b_^B==X&$V*BSXV$6&gH=m5#`9xZ`0Ow4Ayzw`ReQqZmeutxT zz^1DeS|>T9oPKwRyCTSuewHRYOVhWw=SI7W&ZbLqG^Lwf!lPT=9-HYWKCyY|6su0% zr`a^0MkYq}x!e2>PApIBxmWrqZt$v2rdMtMr{zqvUw^vtnkH_ut9`ojQ zc$zhGo;Eve%SZh3x=lg?)LY47QW#Lo7d=%j{+)*#p8G^W`sl3|5XXo z@K6q1?fT+o4wbDHfdPd;L9YEjvub?M=BeM#s=ww!Io}Fj&V_ZjYZ{EikEcQobo=k~ zrolwqISoc(=QK!#4PvwaHu+zEF%8CtZy`HY=Fu42B5nOFF2*R#haHiy%@1*9J{hy^ z{`(o_ph6EWFNZ4Fh0m2kF6_n+%Asl4ZZbv1ezhs3LxiUm(0a`aU|G~23DU~e`aIfP zht}d{Y!Ci+0TH(sVIgQ?F9~~dP+|vh)j}Avs8=GFQ@|o1rma;YUo1mRwYXsrqpzkY zyH^6aj6pmdP`lMj_N?DX_e!C+M*4VWAtb?JytWV~%NHeh9giM@F<4arcfuufR=^Bd zk>OdqyPSA-yaGl-7=Ba%Q+TY5Nx5X~rc}~c0#;WN#fjKm35Ae^Z&pGkf}!$c#tx&j2x?A5N}?#beG8&+FW)bXS?M5G;hH6?-UTBok-1%IqvYRSPL_4o}v?^5N&n z0n~Zyx*v2XV|_TS4wk|h+*}8}a26NT!y*FyQ9aCp^TvDiFa_YE5xE35a`+4GSOz*> z_;=`VUXr$UtMNwhcOX@Q%Z9xH;zHmm7B>^c+ijo?x}ShWITn48oKnh*7N@kJdE-5nccw}~pw3;HMC_L>TwvaM?`WB2e5}hzrh6JOo z9j<_sNDE&+O?sE}ds^1zg$GHsu6bcHq!<}KkN`4`oDTSP97RT7ZzR^FWA{T46AmoK z6qU@(fe|!LujBEhhoBfT@$-k^aZ=8QHo+$}vGZZ@NK&|N-=1(mrT}o>vkZ<6dPat2$DGV^?;7Q>469w z`2@6*(7yi!e4oOLke%>D7=e%Mgf1dw*e+-rlfWFc{>;1I>z4)7XWbTqbOa~iv8Q1~ ze1Dwqjhq>me*vjoBSXVrk(q*e%uJblQPBDZy^iTHW(H#dzfZ84PP*S8%@=7QP1X8? ziY!MUG`%f(lpdN1cO%|$Cvo_n)2WXNz41@T48-Mr*EsXmyt{RU$4HL)<3%Y>TmNKRhRq z3!Qy{GQGY7MA5wmNG3gaDuyS;?U5m0#LB`ilBn%gioAPd;d}AV2cR@MMT#+9ChQW6 z#M2tPu;^td$lNWScM~@#NuhfttLvn0X}z>T+9Yk0wo5${+e5>=M`n9vj9LOo#;;z6 z(J=fa8nsZGa!|bOm07R|2s#l1tvxD(I{|af#VSMKmXu65s^m8|1K7T9b^M1_d z{g``t=>T2CBfZc=p6u>dVLAl|zkLa`J-Vnev%zh1)L|qb_xhymKiOpP< zvFde-cYgL7Y>akFkP}FU=4;+olI(SZHt<78_)3QP#okVrwM4V7qUuF86(pVH)4#&c z4#OG_%GPxwQ*rYfkUU9IZsjg0#N9B3xGVngAqu0PSe{-)DWis{5l z=$Oo=C<8H8rhKIoavSC+U>gvHm1X7~&MNbSlECQGWf&U@DgdG(4KW4osouon9*v|4forX&X| zGE^neygo|$ZTi~&v6yC&ftFBFsB}T7w(oWDgG$VpeT;wq|5lDKY*LU*E`MP0A!La%e;ZqxpsJfi*_4m?FEv!o*EP+8^@S5QKA( zJ3h+d7euUO;IKxS&{y(VEZuN=O2F{<;a*a(AG{BfBN9y<`SMlHlJFPr!ybwj+dd%E zm*Pj69zdBn2xYp7GV&P96e#H_*AF`-06TpU>=YCB*cCQiz|vD8E0oCzK=<-9I(&5>$uV%`GZYJ{@5~xRs9Z*iRvYEb?_HR$ z3EOYtY0s@Z?eX)p$KsDWBd)==F}r9?qCd9VB7VX~6|g<#O&SF?V(|-Pes+HWwfv>P z#G%(=I~>Gkufy1!gEuYOd+VaTgo^|zE^=r=zA^JlY64NNmHQPWa4!D=E53%Q#)+@t zJ;-UQAS+UAqt4MB&E<0YO32nbsT5A74&Ru3>!#{2w8Ag#qFFY zb|tY=8tk8B<{)e=n_U_x9@e?twXJTCkBwsmgT^Y{t`?TeN~x?M+LvW(t-5aW(w9R!8Y`%!IER)d582*9 z_*ptD2y9=xsd2k~Yyw+R?(%jtH&aQCK*V^k-7%4s&S@33S67Qpc;`|%(#A4~u;5^^ z!=r1q&a$-@>I8XNCcZ84_{=OeyB{LRrfgPC82X2UVqrNLCG+_BTvj3K>Lq%o+hwDD zyIrE@%qC&6%twsNW3z))2TO5mve;^^OKbDC(k7WsCTj%B3+9a>MgZ zg*QMUzM=3^C_>4?7sD*9xA0k1vDjwe3!oT3vhc}Jg0Ufd9?Zdp5S|UCxG98B=xPdV zC&-3@mPwiXNpNsbjrQB>j;fv7!G4v_$`Q)q4f1@h{I)v@N%TTJeO3Qt+FR1 zmG~+ka45|sD%pb28-am$S@8>@ecucC?AVkV)#txF({iJ2g9f+ zR|bV4fjZm}_-M7s@qC2ftab6Ez9)2|Xc$g8%{e zjm~EPZWndNxMrNZlebFbIP=ER7ITeLV|h45#>Uxk{E&Q`Pf3iPG#Bz{5OB^3ig9-oVmc{~i~vA2xB4hM~U=JI-gv&NBmoGWn7 z_<4n(yw7-`iq~-}f1j=9EdbYz;u>B;wOZwV!2}hbuI0Z1#cW*en5-7Lrh}=00yBf%brx-6c z@Gm9UZM>-Q)e>dssXG6XO19&g`5uajPc`!_Sd2HCNrab-^cEf?kI)nrSsF-C=aV6$ z=!BB=t5~;+|D7V?g$}-ghd2vYX=`eN$H;){86I&yH0*hvV@7#(2i@{`7hRE>(McKgb0>y3yGzKFvHW5pVw&YSS*wfrgAW-RRF6M+a`b1$ca zC;`uZmsb*dv+v`L#NJ2lBVW{Oym=pgUYwOx|HktH4jWH(@o1nH$Se2r-%tz0c<_64 z%BAzlMcn!TVZVsy9w3eR3yxgR-}Q5F0VEm)Kj5JX>1WwS?x23d&o=TC01Do>i7%te zyPLRGQY>Lu@G##+_4^MW=2wZH(~t0SDbbRI>Sn%#j_umb{ACFWjm$@RFC->W9lx~R zMGaw(S94Ujt!4wZ5+Z?h7a1v!i9OZhBgo&P2!HwGJYQB}1v~jH$Pq(XBZD3@* zl%Hdc%%j@9*Y4dQ75E`4DW0X=(bu_2Do8USOrO3@ip(&d{Dx$^6xl>^pQlBy^Jp$F zHNa^VGv1Y4z8*ZkkEcVo5x$?NNK}~3{TV+Edko6~eh-jmX@8lYp{}zr&)`Lz48jk3 n$qo?ML$8u4?J@Sf%BhdkYg~Ap|2CAs4j$*fk5PIhdno%KUwgpC delta 10291 zcmbU{4OkUb+UGqpbG-;EC@7fdprSy45MpScprE3FAo#@fa zx&EAJwT)KSHGhf|mSPu()Ja?8rfc5Sz5U6K2SLf;yH!0S^Q)w*WIuX>-!g0nWN2^-GQg&W-V(PB`5LmOi3 zcRX{^zIVd)!272pAApU|y!ZV#5%b47e5w*q>*|A!C2Bx~v2M*%*X;8y)10c$p9fLF zoLfk5v2uz5CM&W>ZIMmj%%*GR*0pGXraFtQD5i{nq3pXPh?SdwY*O^g_1*w7iv%pn#xfdxssSo%9>k!^3ewaZvbS0F zA!)BClVm_;NY-*olUHu@s{whD;&79RBEQzXt- zs=liiL;)n|2kh_?3@GNSR{epxF!p%XnM9M=zQ-Vj9sd^kgUr58!u?L_2SeDY5ilU` zt=Vd`3#B{ToLb2V`ajmUJt|H-8ph7{ zrorGiIYn`zR^tx1T2nI%O=+>wePDRTK=HvMLwH=tRF9MIe3%1cvvJv(g+6t$>IfK9 z3I~Y~&Nd!@bV?J~Xs1tU>E^o5To0>ihn!LI;>LLv#E;-%?NKlTlJvhtLA3vf@A7VxPr69q3q zFI=W75S6K^kc=*ybwtpi?5R|UkB^0D51OZ2fc#l#{$5W$XEvC}{%ogdtZ5MLz@)7( zNWRWVFBKrgf)C~)wJbI#4RYGJs|@)&!=rY)yO7gT%>{rW_&cf8keO3?eaR7_&-JPW~G(;aDJruC zmm9p?YZ@`O+a>rDf%DfhL?Q}P_=-!$7Y3)Ept~N$>TT$jqnzSY8l2eunV!TO26l>5k+oLNmoS-5_S9{&6 zh!#YEkf6Xicfd_tmwM;N7n++~=g+ZXyWP~Tco<@86&k<~274m2mTb6G_|G3$Y55Hc z42dWdqWy-~L`87wF*#65GXvN2aZR2OeH~NmFLoN=Gn%wXDcRwa!6R4agaRygu-$4M z7PRct{0{Fj6_xPX5Nt)0Op<_hfm&(CVk7$hXLD%K(u~BwBP+`k&4nhw!>_5e>B)v= zSa=bk5p87oqm>(FM9cL}k^LA^a8AIdc>Ick>miG~1?K&~)cgtD5FVpBmj6@1xeawI zdB}vU5jlZtH#fo1_JxDqhKcf2Xk{I_FoQjji`rxx2RFkiJ}{V-jf2rFdmN5FG!BL{ zKfgOM4n{?;Ll<4*QwUinu&-litR8(UY>9^T=CqflA&O1reV^$V2-;cZbf|!>Y{hiQ zh7R`hbXe5C105~~V{gzCC`QjgNC!(Qg{6JBqkAh^juM>eQX2hiXeko?eJRA#odSDp z7kYd;19Bqva2MMtkWMyn1`M0oC7_|?lPHL7Zo&kvRD{?{enTKuU519=C4g8$AU@!2 z)*OCx;o%t8CD^act=Vo2!ElhhHUq|r=LC43!G0LVl4rtD_>2|Lgb4twZ6@3b64Pfw zt{5Z2Hg@3zB(R7wNJOVSx(u>tg2*mpqNi^ugP~%g2umz*$Im!A~!+Zp8tcHUKoKyp|5O`?~OoFp|XAR^6 zJtu5fACsa-&xLvlA2a`aQ1NnRKBE0h|Mz?-6yUsGcn1vZC0-DqlC69h^B0c;9M?Xk zvp6SA=`q&qgdNvDX0bXId_Bex3r=f*vGl6IW@5TT&kO90qp0R@G{C5s3pc1mWFw4* zODv}mGKXCeV16?iy&9-n?r=|-Fx~5N&Q=w_*F%O8a*3^MM4??_`xtM7F9C2I9VURw)~qG2{Z2w&jTnswmg8vXToG@DP>!9$R%FM9|+5QSVZ znw>g|TD*7zw9)Y*J3I<~f8<7pgpK;3jWAclNa%SKG=KvA-F8T#vD1q}6^@nVtIi(9 zh<&8xj#=kc)QnfYMfYNqCW&m}7KmcP7Szl*QrrWHk=ajTkQ-HKc}Gmt18mV_5I1BX zarS^TYM@Zo?qNj?a&DZ1=2y1`{1DIH+yeLDKF`|<_o3_j^Hyj>$5r<@G}9r3op}PK zY~<-9SYijbA(5@^z)aZ~+JTkS+$YiL7*7SP>q(f5qPzSgY{S)T{Wbg?r8;~YQW?%R zZvzL6VBc+ng>)og*&9$u1)`bQuip;0(ISy&n6m!mc1Xj-f+g&L%}}E6+X2%k=FpdS z!kqr)B0A41k>C&W_q;qZht)j=wPR~VD4ybN$sf(jN6mrhVZet8X00ej;V{od7Km5^ z;6Kc9@t;JA!oGM4?f{jQ?!t@4ezOaU3yq!J1(|v1-=ZMi=IvKwXl`EH1X;O0GW8w3rcZ1En*!GQJn9>|9k?EO8s%35aI zi{hI!+a)Be<7NZJ~WtB?P6@Sx2 z!oGSAZqg6`9!p2`&sm3HkZ=W^(!)CHnsNwj>MA;=Loo9ekZu&;RW3eBLhJOaaeXLE|F z63OLAvf{>D^9y>+AJ69}u;im~d+!1cK3*b4CU}VjUfKh^guQ+gvSTj@LnbRO#p6)n zD6xZGb6ybG;bV}{yOiT7kx03Tqsqcj+XF|Hfn&fN>3SS<*o((taLgR(`qb;IV<7jg z<(R7^vOwBg3xoBXKf)w|DiRwr9h*&$97h{aS?6&`>!(S$ADYA+3tr-RXc@xF&SLoN z_X-qYF;(>nxam@ZLH6}4(4PjS4ToRLjrmtF9AJ5wU_Z&?=Et?8pgp zFRORpD{15!KDtgK_`^LH|3=pHflU&@9}eWTOQfCkKLx3Mx8fdEs=j*7<>IcO^c0MV z>@a|~OYvkoyXO?#Om|9VXnyGwY6sn8FnQCb=Dv6KV62l<`4nA1-J6(rA2iT(NqOWD zd*n@+l5<#sbi>T6)qv5HIUG`49N->%3paV3W&ERC$Z>POPFgZJ(}UDW7IPXU*!UKV z!?f-3w;*>^k=2XAPKkmIL2R3<%fn-_4Ln0XEdleMhCw(5rx&s4(-_=vU{vfG&iYA- zoHdxC7Vv0uE4R&!Ni1JB?VU0_9LAUWrf)YExgk#2BaSCA8QUi>23qoN4TZ$AT$ zIItO8c#%l_CMxCKJQ88!rZl>b&D@e0Tem6pF`zHzkr=$8_!MU|*}n5V?7O+yNAJPd zJ_!b&Efj;^uN#*GADZ_vrm2JW1_JGYc zV2@rTxf~Xs#P^(PDL~VJYPEm#}X?fV92^IKM{Y?SDcFY&_>p zMSJC5`$=5sYSHB2hmdY@3Y|TUCv3C+DMf-O<(&V+!}!m%Ojt_Z()w^ z`V^h3P;0ZZ3+FNA`{n1b7#1+w7f^ch0$b00QfxQulY*#mpOnz#hrfWzKB}=+NysBw zXdKSNT`2IMoriSl<_u4UrTSOrVW&WYHZ!|??gG4u`NDnQz+~ELjeY(N{FAP*9vUvf zKuj7|T*Ulp9lLN5CSo#>bqTAKcGi3eGUzHp5(8ITl33G2600p7(mx*nuJT*v+ao8)#J67Y{~wecVKGWn##xJePo zc8mC9-@*~t$rfIQYP=Y;U%CwKu!kvEV0g}+YY2DVNVpRbqOkczwCCrME)%a@czrI6 z?u3wd_)j@ECb7Q)c?{%PJ$~#*@I!!uHn?cT;2XQm<{3d!E~Zomui-NhE1m1QUWsO* za|rpQhi&Ar$)^6{6?g?8N#FTBYAv;2W=F5WxM_>h@q9&dG+l1-23=0<%eb%$fG0oo zc=oa6+HNE^ZWi$ap*rIP$~=WJTRw|2*}jbstM~mE{1MuUv8z(9@=l*0Be0`Mbz-j9 zgoBmlp$+zp%~Q0{+XONl8))I-3U8nsZEi3rEA=cxn{@VkNp9*@BYsqrWAMj%h+P!xr%bR(3j>x1eJPGlKhLbk!q={|A zNimLv-_L6Cs6H}+6c$_Ury8%fy263ceP}>LsgxAln5C` z@+{6mghrG3QxuQK8>mrzZq0+oQ^i7WJa_!%P^%O+7e zs4bUZ4*FCwjxVa(qcr=QklkC!{LuYe&FezB_^0~3KUF;>kIcuaJJ`^m zIW&}R7=fRRC)103*bl|FyXKgM&Cdpxw}ngy(<;Z7fu=Z}zJcVn@L**S8~To!YAeZS zpHn&%tIgg5t%iwgp+FbFB=)>Oi?I;_BCU!j5`S*;&DO_bVeJ*^2$;+^h;#-_VIPWg zEEKZ=5}k&vsM{qv2BxyL5>3mUUg0gbPE^BknkK$c;SG0DvCoc*Er(2vTOvOQ9>^+> zW73~wrz0Vn?T*9}Y>}PziwtFs1iREr?exU~FvP0bG1s#)Qr2DPWaR1Yf<8q2S=nu&CsytE7O0Bd&LVQo*jY(n?9|=uuw#-{LT9!XN zxw_3Hi`gHZN|0XNvqOznJPO7)*Uruhkv7eU*t@4$i~e3^%D(#CNXoDP z*T3vXKZLd>ga51_Sh*9KyzAQIE%WmLeiqcgW#i05Lk=_5y|^gx(;r`)aW=%e=e~vJ z={BCMhYqDO=ZC$f6p{|7_sDzqY<`)YgIj~vBu?G1vhTaD4I`$q$ zKtJpOmOw!-tL+z|-!Xum6Ja}>7*8LC!}>e%^hJOe{n5d6DwHn30{i;*-~zM4-rB2Y z%EbY%i>?-22cB2b1^VbAw23!G9vMd4u_u=%(@21w`nV+eta#n$CHlXTX#|BX-JU{A z0XxRCQ|Y_}>{NzZ&1S=pq}ma67yGYN`cLR$FQm~pjM}Hu=)a&}xeZsTV0~$DU(P;C zr<<=M!yd?>NtmGR&Oqmr#Li{VLQE%zXVP>`0B2>=TyeD&>ce+r(nL&!UdW_bylIe0 z3!sW+jzK@Qlr0=XzZbU(MZlCSI+HbLQIYM=qHp5rR%X*$3VZbTa_A!f<@%CbT1PRH zJ#i}+aw&UoJk1A1KR%w`4gC)aBtC00KVY1vYK=_+zseTeMt_48#C-Y;W}FA|>7ofM z#B*Yk@ySj`i&~$PVJSd{53N~qt3UEepHV=wK`avNgtbkipX1V(OrkI0p5zqL^?1B; ztdMTOHd%cU-HU6DnM^NZ#@BlaeI7dXm!{A;0H^iD5=tdFqsue+3Qp?hX3`o8=k#$E zv=QKv{`f398HB6iN-hkU303rE0R7b}x(d(&E2`-^ykym&s>SHXYiNIpy70kVnl50; z5b+aq$L9nhrc|r02L8cEsdGzop^bg;6TU-w%h)(r4Atu>!V~e_l_& z$7a?_2i=L)Vw#ikGyPg8?&D|ry-qy(7_>kl(Z#b(33YeYFR;!g`VXuxeyhkg zR$7jtf3TI-q3FM8Mg8j1hy0Z80#u@x@1%JE2X$CM2LPVO#QluEj^{D@$8FS3g|pH* z_Vp^Xmvd~w-KbR`vzEK*+qkNVdoaW%=4YZ(;ZYhvTAqG8pGK@?l*#($>Z5yq z(VbWD2g}W->JL3m8~DmEb{J;hu-+P+A1>5zf_tC|G&VTUl z=xO{MK>vC_#Z#kBeT = { + 0: "Ed25519", + 1: "Sr25519", + 2: "Ecdsa", + 3: "Eth", +}; + +interface NegotiateRequest { + owner: string; + max_bytes: number | bigint; + duration: number; + price_per_byte: number | bigint; + replica_params: unknown | null; +} + +interface SignedTerms { + terms: { + owner: string; + max_bytes: number | bigint; + duration: number; + price_per_byte: number | bigint; + valid_until: number; + nonce: number | bigint; + replica_params: unknown | null; + }; + signature: string; +} + +async function negotiateTerms( + providerUrl: string, + request: NegotiateRequest, +): Promise { + const res = await fetch(`${providerUrl.replace(/\/$/, "")}/negotiate`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request, (_k, v) => + typeof v === "bigint" ? v.toString() : v, + ), + }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`/negotiate failed: ${res.status} ${body}`); + } + return res.json(); +} + +function hexToBytes(hex: string): Uint8Array { + const h = hex.startsWith("0x") ? hex.slice(2) : hex; + const out = new Uint8Array(h.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16); + } + return out; +} + +/** + * Build the `{ provider, terms, sig }` args shared by every signed-terms + * extrinsic. Mirrors `console-ui/src/lib/storage.ts::buildSignedTermsArgs`. + * + * The inner of `MultiSignature::Sr25519` is `[u8; 64]` which PAPI v2 encodes + * as `SizedBytes(64) = Codec` — pass a `0x`-prefixed hex string, + * NOT a `Uint8Array`. (See PAPI v2 migration doc + console-ui's working + * implementation.) + */ +function buildSignedTermsArgs(providerAccount: string, signed: SignedTerms) { + const sigBytes = hexToBytes(signed.signature); + if (sigBytes.length < 1) { + throw new Error("signature too short to contain a MultiSignature variant byte"); + } + const variantName = MULTI_SIGNATURE_VARIANT[sigBytes[0]]; + if (!variantName) { + throw new Error(`unknown MultiSignature variant byte: ${sigBytes[0]}`); + } + const sigPayloadHex = + "0x" + + Array.from(sigBytes.slice(1)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sig = Enum(variantName as any, sigPayloadHex); + + const t = signed.terms; + const terms = { + owner: t.owner, + max_bytes: BigInt(t.max_bytes), + duration: t.duration, + price_per_byte: BigInt(t.price_per_byte), + valid_until: t.valid_until, + nonce: BigInt(t.nonce), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + replica_params: (t.replica_params ?? undefined) as any, + }; + return { provider: providerAccount, terms, sig }; +} + // ─── S3 Buckets (console-ui) ───────────────────────────────────────────────── export interface CreateBucketOptions { name: string; - minProviders?: number; + /** Provider HTTP base URL for the /negotiate call. Defaults to env or localhost. */ + providerUrl?: string; + /** Provider on-chain account. Defaults to the signer (CI's Alice is both owner and provider). */ + providerAccount?: string; + /** Storage capacity in bytes negotiated with the provider. Defaults to 10 MiB. */ + maxBytes?: bigint; + /** Agreement duration in blocks. Defaults to 10,000. */ + duration?: number; + /** Price per byte per block. Defaults to 0 (test fixture). */ + pricePerByte?: bigint; } export interface BucketHandle { @@ -19,11 +129,21 @@ export async function createBucketViaApi( opts: CreateBucketOptions, ): Promise { const api = getApi(); - const nameBytes = new TextEncoder().encode(opts.name); + const providerUrl = opts.providerUrl ?? DEFAULT_PROVIDER_URL; + const providerAccount = opts.providerAccount ?? signer.address; + + const signed = await negotiateTerms(providerUrl, { + owner: signer.address, + max_bytes: opts.maxBytes ?? 10_485_760n, + duration: opts.duration ?? 10_000, + price_per_byte: opts.pricePerByte ?? 0n, + replica_params: null, + }); + const result = await submitExtrinsic( api.tx.S3Registry.create_s3_bucket({ - name: nameBytes, - min_providers: opts.minProviders ?? 1, + name: Binary.fromText(opts.name), + ...buildSignedTermsArgs(providerAccount, signed), }), signer.signer, ); @@ -84,10 +204,16 @@ export async function cleanupBuckets(signer: DevSigner): Promise { export interface CreateDriveOptions { name?: string; - maxCapacity: bigint; - storagePeriod: number; - payment: bigint; - minProviders?: number; + /** Provider HTTP base URL for the /negotiate call. Defaults to env or localhost. */ + providerUrl?: string; + /** Provider on-chain account. Defaults to the signer. */ + providerAccount?: string; + /** Storage capacity in bytes. Defaults to 10 MiB. */ + maxCapacity?: bigint; + /** Agreement duration in blocks. Defaults to 10,000. */ + storagePeriod?: number; + /** Price per byte per block. Defaults to 0. */ + pricePerByte?: bigint; } export interface DriveHandle { @@ -98,18 +224,25 @@ export interface DriveHandle { export async function createDriveViaApi( signer: DevSigner, - opts: CreateDriveOptions, + opts: CreateDriveOptions = {}, ): Promise { const api = getApi(); + const providerUrl = opts.providerUrl ?? DEFAULT_PROVIDER_URL; + const providerAccount = opts.providerAccount ?? signer.address; - const nameBytes = opts.name ? new TextEncoder().encode(opts.name) : undefined; + const signed = await negotiateTerms(providerUrl, { + owner: signer.address, + max_bytes: opts.maxCapacity ?? 10_485_760n, + duration: opts.storagePeriod ?? 10_000, + price_per_byte: opts.pricePerByte ?? 0n, + replica_params: null, + }); + + const nameBytes = opts.name ? Binary.fromText(opts.name) : undefined; const result = await submitExtrinsic( api.tx.DriveRegistry.create_drive({ name: nameBytes, - max_capacity: opts.maxCapacity, - storage_period: opts.storagePeriod, - payment: opts.payment, - min_providers: opts.minProviders ?? undefined, + ...buildSignedTermsArgs(providerAccount, signed), }), signer.signer, ); @@ -117,25 +250,7 @@ export async function createDriveViaApi( const created = api.event.DriveRegistry.DriveCreated.filter(result.events as never); if (created.length === 0) throw new Error("DriveCreated event not found"); const { drive_id, bucket_id } = created[0].payload; - const handle: DriveHandle = { driveId: drive_id, bucketId: bucket_id, name: opts.name }; - - // create_drive auto-emits a request_agreement targeting the matched - // provider. Only the provider can call accept_agreement (the drive owner - // can't), so we wait for the provider node's auto-coordinator to settle - // it. The coordinator polls every ~6s; accept_agreement finalizes in - // ~12-24s — typical end-to-end is 30-36s, worst case ~50s under nonce - // contention from rapid prior cleanup. 90s absorbs the worst case and - // adds <30s to the worst-case test (which we'd happily pay vs. a flake). - const start = Date.now(); - const timeoutMs = 90_000; - while (Date.now() - start < timeoutMs) { - const bucket = await api.query.StorageProvider.Buckets.getValue(handle.bucketId); - if (bucket && bucket.primary_providers.length > 0) return handle; - await new Promise((r) => setTimeout(r, 1500)); - } - throw new Error( - `createDriveViaApi: bucket ${handle.bucketId} primary_providers stayed empty after ${timeoutMs}ms — provider node may not be running or not auto-accepting.`, - ); + return { driveId: drive_id, bucketId: bucket_id, name: opts.name }; } export async function deleteDriveViaApi(signer: DevSigner, driveId: bigint): Promise { From 7195874ee1db9fd541d30f687be7cf335d67e8fe Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:41:50 +0700 Subject: [PATCH 27/44] test(provider-node): derive expected /info provider id from signing seed The /info endpoint now reports the signer's real SS58 account instead of a placeholder, so the integration test derives the expected id from the //Alice seed rather than asserting "0xtest_provider". --- provider-node/tests/api_integration.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/provider-node/tests/api_integration.rs b/provider-node/tests/api_integration.rs index ab924065..0f245f53 100644 --- a/provider-node/tests/api_integration.rs +++ b/provider-node/tests/api_integration.rs @@ -7,6 +7,7 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use codec::Encode; use reqwest::Client; use serde_json::{json, Value}; +use sp_core::crypto::Ss58Codec; use sp_core::{sr25519, ByteArray, Pair, H256}; use std::net::SocketAddr; use std::sync::Arc; @@ -20,6 +21,8 @@ struct TestServer { client: Client, } +pub const PROVIDER_SEED: &str = "//Alice"; + impl TestServer { /// Spin up a server with `//Alice` as the signing key. /// @@ -27,7 +30,7 @@ impl TestServer { /// because a real sr25519 keypair is available. async fn new() -> Self { Self::with_state(Arc::new( - ProviderState::with_seed(Arc::new(Storage::new()), "//Alice") + ProviderState::with_seed(Arc::new(Storage::new()), PROVIDER_SEED) .expect("//Alice is a valid SURI"), )) .await @@ -97,8 +100,13 @@ async fn test_info_endpoint() { assert_eq!(response.status(), StatusCode::OK); + let expect_provider_id = sr25519::Pair::from_string(PROVIDER_SEED, None) + .expect("Invalid provider seed") + .public() + .to_ss58check(); + let body: Value = response.json().await.unwrap(); - assert_eq!(body["provider_id"], "0xtest_provider"); + assert_eq!(body["provider_id"], expect_provider_id); } #[tokio::test] From eadd3c45b82c88211c26547908cca2022b92f75c Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:41:50 +0700 Subject: [PATCH 28/44] chore(weights): regenerate weights for the signed-terms extrinsic surface Re-run benchmarks for pallet_storage_provider, pallet_drive_registry and pallet_s3_registry on both runtimes after bucket/drive creation moved to the provider-signed terms flow. --- runtime/src/weights/pallet_drive_registry.rs | 50 ++- runtime/src/weights/pallet_s3_registry.rs | 83 ++--- .../src/weights/pallet_storage_provider.rs | 320 +++++++----------- .../src/weights/pallet_drive_registry.rs | 60 ++-- .../src/weights/pallet_s3_registry.rs | 85 ++--- .../src/weights/pallet_storage_provider.rs | 320 +++++++----------- 6 files changed, 335 insertions(+), 583 deletions(-) diff --git a/runtime/src/weights/pallet_drive_registry.rs b/runtime/src/weights/pallet_drive_registry.rs index f6226f7b..3765c3a5 100644 --- a/runtime/src/weights/pallet_drive_registry.rs +++ b/runtime/src/weights/pallet_drive_registry.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_drive_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-06-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `39a82f1f1afe`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -30,7 +30,7 @@ // --extrinsic=* // --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_drive_registry -// --header=/__w/web3-storage/web3-storage/scripts/cmd/file_header.txt +// --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt // --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 @@ -75,13 +75,12 @@ impl pallet_drive_registry::WeightInfo for WeightInfo Weight { // Proof Size summary in bytes: - // Measured: `2356` - // Estimated: `18000` - // Minimum execution time: 215_519_000 picoseconds. - Weight::from_parts(224_400_000, 0) - .saturating_add(Weight::from_parts(0, 18000)) - .saturating_add(T::DbWeight::get().reads(16)) - .saturating_add(T::DbWeight::get().writes(13)) + // Measured: `1301` + // Estimated: `11515` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(64_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:1) /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) @@ -101,13 +100,12 @@ impl pallet_drive_registry::WeightInfo for WeightInfo Weight { // Proof Size summary in bytes: - // Measured: `14910` + // Measured: `12052` // Estimated: `1053490` - // Minimum execution time: 922_984_000 picoseconds. - Weight::from_parts(944_361_000, 0) - .saturating_add(Weight::from_parts(0, 1053490)) - .saturating_add(T::DbWeight::get().reads(121)) - .saturating_add(T::DbWeight::get().writes(120)) + // Minimum execution time: 331_000_000 picoseconds. + Weight::from_parts(367_000_000, 1053490) + .saturating_add(T::DbWeight::get().reads(108_u64)) + .saturating_add(T::DbWeight::get().writes(108_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) @@ -119,11 +117,10 @@ impl pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -30,7 +30,7 @@ // --extrinsic=* // --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_s3_registry -// --header=/__w/web3-storage/web3-storage/scripts/cmd/file_header.txt +// --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt // --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 @@ -77,11 +77,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 36_128_000 picoseconds. - Weight::from_parts(38_407_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(7)) + // Minimum execution time: 59_000_000 picoseconds. + Weight::from_parts(67_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -93,11 +92,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1169` // Estimated: `4315` - // Minimum execution time: 21_112_000 picoseconds. - Weight::from_parts(22_421_000, 0) - .saturating_add(Weight::from_parts(0, 4315)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(20_000_000, 4315) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -107,11 +105,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 55_484_000 picoseconds. - Weight::from_parts(57_449_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(54_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -121,11 +118,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 39_774_000 picoseconds. - Weight::from_parts(41_252_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(40_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:2 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -135,40 +131,9 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8205` // Estimated: `21362` - // Minimum execution time: 60_584_000 picoseconds. - Weight::from_parts(63_152_000, 0) - .saturating_add(Weight::from_parts(0, 21362)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(58_000_000, 21362) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } - /// Storage: `S3Registry::BucketNameToId` (r:1 w:1) - /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::UserBuckets` (r:1 w:1) - /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::NextS3BucketId` (r:1 w:1) - /// Proof: `S3Registry::NextS3BucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::S3Buckets` (r:0 w:1) - /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_s3_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `2356` - // Estimated: `18000` - // Minimum execution time: 106_427_000 picoseconds. - Weight::from_parts(112_236_000, 0) - .saturating_add(Weight::from_parts(0, 18000)) - .saturating_add(T::DbWeight::get().reads(13)) - .saturating_add(T::DbWeight::get().writes(9)) - } -} +} \ No newline at end of file diff --git a/runtime/src/weights/pallet_storage_provider.rs b/runtime/src/weights/pallet_storage_provider.rs index 5537179d..1966363c 100644 --- a/runtime/src/weights/pallet_storage_provider.rs +++ b/runtime/src/weights/pallet_storage_provider.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-06-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `39a82f1f1afe`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `192.168.0.105`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -30,7 +30,7 @@ // --extrinsic=* // --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_storage_provider -// --header=/__w/web3-storage/web3-storage/scripts/cmd/file_header.txt +// --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt // --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 @@ -56,10 +56,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn register_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `221` + // Measured: `107` // Estimated: `3825` - // Minimum execution time: 28_562_000 picoseconds. - Weight::from_parts(30_074_000, 0) + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -70,8 +70,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `272` // Estimated: `3825` - // Minimum execution time: 13_497_000 picoseconds. - Weight::from_parts(14_453_000, 0) + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -86,8 +86,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 7_065_206_000 picoseconds. - Weight::from_parts(7_138_759_000, 0) + // Minimum execution time: 3_734_000_000 picoseconds. + Weight::from_parts(3_838_000_000, 0) .saturating_add(Weight::from_parts(0, 2566553)) .saturating_add(T::DbWeight::get().reads(1003)) .saturating_add(T::DbWeight::get().writes(1002)) @@ -98,8 +98,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `255` // Estimated: `3825` - // Minimum execution time: 12_363_000 picoseconds. - Weight::from_parts(13_289_000, 0) + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -110,8 +110,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `251` // Estimated: `3825` - // Minimum execution time: 12_389_000 picoseconds. - Weight::from_parts(13_296_000, 0) + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -124,8 +124,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `354` // Estimated: `3825` - // Minimum execution time: 26_906_000 picoseconds. - Weight::from_parts(28_316_000, 0) + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(15_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -138,8 +138,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `395` // Estimated: `3825` - // Minimum execution time: 17_243_000 picoseconds. - Weight::from_parts(18_323_000, 0) + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) @@ -150,59 +150,21 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `251` // Estimated: `3825` - // Minimum execution time: 12_792_000 picoseconds. - Weight::from_parts(13_750_000, 0) + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_bucket() -> Weight { - // Proof Size summary in bytes: - // Measured: `160` - // Estimated: `11515` - // Minimum execution time: 16_294_000 picoseconds. - Weight::from_parts(17_528_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: `StorageProvider::Providers` (r:2 w:1) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn create_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `505` - // Estimated: `11515` - // Minimum execution time: 50_531_000 picoseconds. - Weight::from_parts(53_103_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(6)) - } /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_bucket_min_providers() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` - // Minimum execution time: 10_220_000 picoseconds. - Weight::from_parts(11_017_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + // Measured: `358` + // Estimated: `3823` + // Minimum execution time: 4_000_000 picoseconds. + Weight::from_parts(5_000_000, 0) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -210,11 +172,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `581` - // Estimated: `4046` - // Minimum execution time: 14_590_000 picoseconds. - Weight::from_parts(15_684_000, 0) - .saturating_add(Weight::from_parts(0, 4046)) + // Measured: `543` + // Estimated: `4008` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 4008)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -226,8 +188,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `427` // Estimated: `11515` - // Minimum execution time: 18_753_000 picoseconds. - Weight::from_parts(19_875_000, 0) + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -240,8 +202,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `497` // Estimated: `11515` - // Minimum execution time: 19_984_000 picoseconds. - Weight::from_parts(21_235_000, 0) + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -256,11 +218,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `985` - // Estimated: `4450` - // Minimum execution time: 44_190_000 picoseconds. - Weight::from_parts(46_371_000, 0) - .saturating_add(Weight::from_parts(0, 4450)) + // Measured: `947` + // Estimated: `4412` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 0) + .saturating_add(Weight::from_parts(0, 4412)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -280,77 +242,33 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn establish_storage_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `612` - // Estimated: `4077` - // Minimum execution time: 37_301_000 picoseconds. - Weight::from_parts(39_124_000, 0) - .saturating_add(Weight::from_parts(0, 4077)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `354` + // Estimated: `11515` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(48_000_000, 0) + .saturating_add(Weight::from_parts(0, 11515)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `StorageProvider::Buckets` (r:1 w:0) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_primary_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `774` - // Estimated: `4239` - // Minimum execution time: 38_262_000 picoseconds. - Weight::from_parts(40_080_000, 0) - .saturating_add(Weight::from_parts(0, 4239)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) - } + /// Storage: `StorageProvider::StorageAgreements` (r:1 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:1 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn accept_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `833` - // Estimated: `4298` - // Minimum execution time: 33_111_000 picoseconds. - Weight::from_parts(35_163_000, 0) - .saturating_add(Weight::from_parts(0, 4298)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayStates` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayStates` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 27_484_000 picoseconds. - Weight::from_parts(29_107_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn withdraw_agreement_request() -> Weight { - // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 29_190_000 picoseconds. - Weight::from_parts(30_632_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `734` + // Estimated: `4199` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(47_000_000, 0) + .saturating_add(Weight::from_parts(0, 4199)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -362,8 +280,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `639` // Estimated: `3825` - // Minimum execution time: 36_014_000 picoseconds. - Weight::from_parts(37_606_000, 0) + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) @@ -378,8 +296,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `742` // Estimated: `6196` - // Minimum execution time: 82_819_000 picoseconds. - Weight::from_parts(85_745_000, 0) + // Minimum execution time: 45_000_000 picoseconds. + Weight::from_parts(49_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) @@ -397,11 +315,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1050 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` - // Minimum execution time: 83_246_000 picoseconds. - Weight::from_parts(86_337_271, 0) + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(46_659_183, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 177_580 - .saturating_add(Weight::from_parts(35_108_278, 0).saturating_mul(a.into())) + // Standard Error: 151_274 + .saturating_add(Weight::from_parts(22_140_816, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5)) @@ -420,8 +338,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1050` // Estimated: `6196` - // Minimum execution time: 80_532_000 picoseconds. - Weight::from_parts(83_610_000, 0) + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(45_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(5)) @@ -434,8 +352,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1697` // Estimated: `15165` - // Minimum execution time: 269_055_000 picoseconds. - Weight::from_parts(275_805_000, 0) + // Minimum execution time: 122_000_000 picoseconds. + Weight::from_parts(130_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -448,8 +366,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1751` // Estimated: `15165` - // Minimum execution time: 258_848_000 picoseconds. - Weight::from_parts(266_468_000, 0) + // Minimum execution time: 122_000_000 picoseconds. + Weight::from_parts(130_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -462,11 +380,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointPool` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) fn fund_checkpoint_pool() -> Weight { // Proof Size summary in bytes: - // Measured: `324` - // Estimated: `3789` - // Minimum execution time: 30_810_000 picoseconds. - Weight::from_parts(32_467_000, 0) - .saturating_add(Weight::from_parts(0, 3789)) + // Measured: `253` + // Estimated: `3718` + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(16_000_000, 0) + .saturating_add(Weight::from_parts(0, 3718)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -485,13 +403,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `s` is `[1, 5]`. fn provider_checkpoint(s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `564 + s * (258 ±0)` - // Estimated: `4029 + s * (2835 ±0)` - // Minimum execution time: 84_540_000 picoseconds. - Weight::from_parts(38_501_295, 0) - .saturating_add(Weight::from_parts(0, 4029)) - // Standard Error: 59_434 - .saturating_add(Weight::from_parts(49_199_173, 0).saturating_mul(s.into())) + // Measured: `493 + s * (258 ±0)` + // Estimated: `3958 + s * (2835 ±0)` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(16_842_932, 0) + .saturating_add(Weight::from_parts(0, 3958)) + // Standard Error: 27_287 + .saturating_add(Weight::from_parts(24_466_296, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4)) @@ -503,11 +421,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointConfigs` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) fn configure_checkpoint_window() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` - // Minimum execution time: 13_549_000 picoseconds. - Weight::from_parts(14_503_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + // Measured: `358` + // Estimated: `3823` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -525,8 +443,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `929` // Estimated: `6196` - // Minimum execution time: 61_139_000 picoseconds. - Weight::from_parts(64_118_000, 0) + // Minimum execution time: 30_000_000 picoseconds. + Weight::from_parts(33_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -539,8 +457,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `397` // Estimated: `3593` - // Minimum execution time: 29_491_000 picoseconds. - Weight::from_parts(31_065_000, 0) + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -555,11 +473,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `893` - // Estimated: `4358` - // Minimum execution time: 36_307_000 picoseconds. - Weight::from_parts(38_230_000, 0) - .saturating_add(Weight::from_parts(0, 4358)) + // Measured: `855` + // Estimated: `4320` + // Minimum execution time: 17_000_000 picoseconds. + Weight::from_parts(19_000_000, 0) + .saturating_add(Weight::from_parts(0, 4320)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -575,11 +493,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `667` - // Estimated: `4132` - // Minimum execution time: 87_282_000 picoseconds. - Weight::from_parts(90_296_000, 0) - .saturating_add(Weight::from_parts(0, 4132)) + // Measured: `629` + // Estimated: `4094` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(44_000_000, 0) + .saturating_add(Weight::from_parts(0, 4094)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -593,11 +511,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `755` - // Estimated: `4220` - // Minimum execution time: 38_319_000 picoseconds. - Weight::from_parts(40_392_000, 0) - .saturating_add(Weight::from_parts(0, 4220)) + // Measured: `788` + // Estimated: `4253` + // Minimum execution time: 19_000_000 picoseconds. + Weight::from_parts(21_000_000, 0) + .saturating_add(Weight::from_parts(0, 4253)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -613,8 +531,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1093` // Estimated: `6196` - // Minimum execution time: 1_131_556_000 picoseconds. - Weight::from_parts(1_143_714_000, 0) + // Minimum execution time: 409_000_000 picoseconds. + Weight::from_parts(436_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -631,8 +549,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1306` // Estimated: `6660` - // Minimum execution time: 112_647_000 picoseconds. - Weight::from_parts(117_117_000, 0) + // Minimum execution time: 53_000_000 picoseconds. + Weight::from_parts(58_000_000, 0) .saturating_add(Weight::from_parts(0, 6660)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -649,8 +567,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1147` // Estimated: `6196` - // Minimum execution time: 59_087_000 picoseconds. - Weight::from_parts(61_811_000, 0) + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(31_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -663,10 +581,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `1008` + // Measured: `969` // Estimated: `6196` - // Minimum execution time: 72_721_000 picoseconds. - Weight::from_parts(75_305_000, 0) + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(42_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) @@ -679,10 +597,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `506` // Estimated: `3692` - // Minimum execution time: 29_796_000 picoseconds. - Weight::from_parts(31_250_000, 0) + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3692)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } -} +} \ No newline at end of file diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs b/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs index a1387861..3765c3a5 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_drive_registry.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_drive_registry` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-06-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `39a82f1f1afe`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `192.168.0.104`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -30,8 +30,8 @@ // --extrinsic=* // --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_drive_registry -// --header=/__w/web3-storage/web3-storage/scripts/cmd/file_header.txt -// --output=./runtimes/web3-storage-paseo/src/weights +// --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt +// --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 // --repeat=20 @@ -63,12 +63,6 @@ impl pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo Weight { // Proof Size summary in bytes: - // Measured: `2356` - // Estimated: `18000` - // Minimum execution time: 210_450_000 picoseconds. - Weight::from_parts(219_748_000, 0) - .saturating_add(Weight::from_parts(0, 18000)) - .saturating_add(T::DbWeight::get().reads(16)) - .saturating_add(T::DbWeight::get().writes(13)) + // Measured: `1301` + // Estimated: `11515` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(64_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:1) /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) @@ -97,7 +90,7 @@ impl pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo Weight { // Proof Size summary in bytes: - // Measured: `14910` + // Measured: `12052` // Estimated: `1053490` - // Minimum execution time: 906_505_000 picoseconds. - Weight::from_parts(926_567_000, 0) - .saturating_add(Weight::from_parts(0, 1053490)) - .saturating_add(T::DbWeight::get().reads(121)) - .saturating_add(T::DbWeight::get().writes(120)) + // Minimum execution time: 331_000_000 picoseconds. + Weight::from_parts(367_000_000, 1053490) + .saturating_add(T::DbWeight::get().reads(108_u64)) + .saturating_add(T::DbWeight::get().writes(108_u64)) } /// Storage: `DriveRegistry::Drives` (r:1 w:0) /// Proof: `DriveRegistry::Drives` (`max_values`: None, `max_size`: Some(215), added: 2690, mode: `MaxEncodedLen`) @@ -125,11 +117,10 @@ impl pallet_drive_registry::WeightInfo for WeightInfo pallet_drive_registry::WeightInfo for WeightInfo` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -30,8 +30,8 @@ // --extrinsic=* // --runtime=target/production/wbuild/storage-parachain-runtime/storage_parachain_runtime.wasm // --pallet=pallet_s3_registry -// --header=/__w/web3-storage/web3-storage/scripts/cmd/file_header.txt -// --output=./runtimes/web3-storage-paseo/src/weights +// --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt +// --output=./runtime/src/weights // --wasm-execution=compiled // --steps=50 // --repeat=20 @@ -77,11 +77,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1301` // Estimated: `11515` - // Minimum execution time: 36_599_000 picoseconds. - Weight::from_parts(38_587_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(7)) + // Minimum execution time: 59_000_000 picoseconds. + Weight::from_parts(67_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -93,11 +92,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `1169` // Estimated: `4315` - // Minimum execution time: 20_834_000 picoseconds. - Weight::from_parts(22_218_000, 0) - .saturating_add(Weight::from_parts(0, 4315)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) + // Minimum execution time: 11_000_000 picoseconds. + Weight::from_parts(20_000_000, 4315) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -107,11 +105,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 54_913_000 picoseconds. - Weight::from_parts(56_780_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 34_000_000 picoseconds. + Weight::from_parts(54_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:1 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -121,11 +118,10 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8038` // Estimated: `11176` - // Minimum execution time: 39_558_000 picoseconds. - Weight::from_parts(41_172_000, 0) - .saturating_add(Weight::from_parts(0, 11176)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 25_000_000 picoseconds. + Weight::from_parts(40_000_000, 11176) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } /// Storage: `S3Registry::S3Buckets` (r:2 w:1) /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) @@ -135,40 +131,9 @@ impl pallet_s3_registry::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `8205` // Estimated: `21362` - // Minimum execution time: 59_696_000 picoseconds. - Weight::from_parts(62_262_000, 0) - .saturating_add(Weight::from_parts(0, 21362)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Minimum execution time: 40_000_000 picoseconds. + Weight::from_parts(58_000_000, 21362) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) } - /// Storage: `S3Registry::BucketNameToId` (r:1 w:1) - /// Proof: `S3Registry::BucketNameToId` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::UserBuckets` (r:1 w:1) - /// Proof: `S3Registry::UserBuckets` (`max_values`: None, `max_size`: Some(850), added: 3325, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Providers` (r:6 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::NextS3BucketId` (r:1 w:1) - /// Proof: `S3Registry::NextS3BucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `S3Registry::S3Buckets` (r:0 w:1) - /// Proof: `S3Registry::S3Buckets` (`max_values`: None, `max_size`: Some(158), added: 2633, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_s3_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `2356` - // Estimated: `18000` - // Minimum execution time: 105_829_000 picoseconds. - Weight::from_parts(111_666_000, 0) - .saturating_add(Weight::from_parts(0, 18000)) - .saturating_add(T::DbWeight::get().reads(13)) - .saturating_add(T::DbWeight::get().writes(9)) - } -} +} \ No newline at end of file diff --git a/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs b/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs index e49595f9..2fff873b 100644 --- a/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs +++ b/runtimes/web3-storage-paseo/src/weights/pallet_storage_provider.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_storage_provider` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 54.0.0 -//! DATE: 2026-06-01, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `39a82f1f1afe`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `192.168.0.105`, CPU: `` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -30,7 +30,7 @@ // --extrinsic=* // --runtime=target/production/wbuild/storage-paseo-runtime/storage_paseo_runtime.wasm // --pallet=pallet_storage_provider -// --header=/__w/web3-storage/web3-storage/scripts/cmd/file_header.txt +// --header=/Users/huytung/Documents/web3-storage/scripts/cmd/file_header.txt // --output=./runtimes/web3-storage-paseo/src/weights // --wasm-execution=compiled // --steps=50 @@ -56,10 +56,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn register_provider() -> Weight { // Proof Size summary in bytes: - // Measured: `221` + // Measured: `107` // Estimated: `3825` - // Minimum execution time: 28_489_000 picoseconds. - Weight::from_parts(29_778_000, 0) + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(15_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -70,8 +70,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `272` // Estimated: `3825` - // Minimum execution time: 13_527_000 picoseconds. - Weight::from_parts(14_518_000, 0) + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -86,8 +86,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `44971` // Estimated: `2566553` - // Minimum execution time: 6_898_779_000 picoseconds. - Weight::from_parts(7_049_685_000, 0) + // Minimum execution time: 3_521_000_000 picoseconds. + Weight::from_parts(3_656_000_000, 0) .saturating_add(Weight::from_parts(0, 2566553)) .saturating_add(T::DbWeight::get().reads(1003)) .saturating_add(T::DbWeight::get().writes(1002)) @@ -98,8 +98,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `255` // Estimated: `3825` - // Minimum execution time: 12_136_000 picoseconds. - Weight::from_parts(13_183_000, 0) + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -110,8 +110,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `251` // Estimated: `3825` - // Minimum execution time: 12_190_000 picoseconds. - Weight::from_parts(13_261_000, 0) + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -124,8 +124,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `354` // Estimated: `3825` - // Minimum execution time: 26_151_000 picoseconds. - Weight::from_parts(27_517_000, 0) + // Minimum execution time: 13_000_000 picoseconds. + Weight::from_parts(14_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -138,8 +138,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `395` // Estimated: `3825` - // Minimum execution time: 17_018_000 picoseconds. - Weight::from_parts(18_168_000, 0) + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) @@ -150,59 +150,21 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `251` // Estimated: `3825` - // Minimum execution time: 12_585_000 picoseconds. - Weight::from_parts(13_584_000, 0) + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(6_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - fn create_bucket() -> Weight { - // Proof Size summary in bytes: - // Measured: `160` - // Estimated: `11515` - // Minimum execution time: 16_159_000 picoseconds. - Weight::from_parts(17_283_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: `StorageProvider::Providers` (r:2 w:1) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::NextBucketId` (r:1 w:1) - /// Proof: `StorageProvider::NextBucketId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::MemberBuckets` (r:1 w:1) - /// Proof: `StorageProvider::MemberBuckets` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:0 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn create_bucket_with_storage() -> Weight { - // Proof Size summary in bytes: - // Measured: `505` - // Estimated: `11515` - // Minimum execution time: 50_791_000 picoseconds. - Weight::from_parts(53_565_000, 0) - .saturating_add(Weight::from_parts(0, 11515)) - .saturating_add(T::DbWeight::get().reads(5)) - .saturating_add(T::DbWeight::get().writes(6)) - } /// Storage: `StorageProvider::Buckets` (r:1 w:1) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn set_bucket_min_providers() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` - // Minimum execution time: 10_308_000 picoseconds. - Weight::from_parts(10_993_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + // Measured: `358` + // Estimated: `3823` + // Minimum execution time: 4_000_000 picoseconds. + Weight::from_parts(5_000_000, 0) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -210,11 +172,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn freeze_bucket() -> Weight { // Proof Size summary in bytes: - // Measured: `581` - // Estimated: `4046` - // Minimum execution time: 14_356_000 picoseconds. - Weight::from_parts(15_657_000, 0) - .saturating_add(Weight::from_parts(0, 4046)) + // Measured: `543` + // Estimated: `4008` + // Minimum execution time: 6_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 4008)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -226,8 +188,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `427` // Estimated: `11515` - // Minimum execution time: 18_675_000 picoseconds. - Weight::from_parts(19_811_000, 0) + // Minimum execution time: 8_000_000 picoseconds. + Weight::from_parts(9_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -240,8 +202,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `497` // Estimated: `11515` - // Minimum execution time: 20_240_000 picoseconds. - Weight::from_parts(21_472_000, 0) + // Minimum execution time: 9_000_000 picoseconds. + Weight::from_parts(10_000_000, 0) .saturating_add(Weight::from_parts(0, 11515)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -256,11 +218,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) fn remove_slashed() -> Weight { // Proof Size summary in bytes: - // Measured: `985` - // Estimated: `4450` - // Minimum execution time: 43_425_000 picoseconds. - Weight::from_parts(45_569_000, 0) - .saturating_add(Weight::from_parts(0, 4450)) + // Measured: `947` + // Estimated: `4412` + // Minimum execution time: 22_000_000 picoseconds. + Weight::from_parts(24_000_000, 0) + .saturating_add(Weight::from_parts(0, 4412)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -280,77 +242,33 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) fn establish_storage_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `612` - // Estimated: `4077` - // Minimum execution time: 36_921_000 picoseconds. - Weight::from_parts(38_702_000, 0) - .saturating_add(Weight::from_parts(0, 4077)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `354` + // Estimated: `11515` + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(49_000_000, 0) + .saturating_add(Weight::from_parts(0, 11515)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(7)) } /// Storage: `StorageProvider::Buckets` (r:1 w:0) /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::Providers` (r:1 w:0) - /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - fn request_primary_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `774` - // Estimated: `4239` - // Minimum execution time: 38_029_000 picoseconds. - Weight::from_parts(39_730_000, 0) - .saturating_add(Weight::from_parts(0, 4239)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(2)) - } + /// Storage: `StorageProvider::StorageAgreements` (r:1 w:1) + /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `StorageProvider::Buckets` (r:1 w:1) - /// Proof: `StorageProvider::Buckets` (`max_values`: None, `max_size`: None, mode: `Measured`) - /// Storage: `StorageProvider::StorageAgreements` (r:0 w:1) - /// Proof: `StorageProvider::StorageAgreements` (`max_values`: None, `max_size`: Some(227), added: 2702, mode: `MaxEncodedLen`) - fn accept_agreement() -> Weight { - // Proof Size summary in bytes: - // Measured: `833` - // Estimated: `4298` - // Minimum execution time: 33_474_000 picoseconds. - Weight::from_parts(35_108_000, 0) - .saturating_add(Weight::from_parts(0, 4298)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) + /// Storage: `StorageProvider::ProviderReplayStates` (r:1 w:1) + /// Proof: `StorageProvider::ProviderReplayStates` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) /// Storage: `System::Account` (r:1 w:1) /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn establish_replica_agreement() -> Weight { // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 27_255_000 picoseconds. - Weight::from_parts(28_694_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) - } - /// Storage: `StorageProvider::AgreementRequests` (r:1 w:1) - /// Proof: `StorageProvider::AgreementRequests` (`max_values`: None, `max_size`: Some(157), added: 2632, mode: `MaxEncodedLen`) - /// Storage: `System::Account` (r:1 w:1) - /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) - fn withdraw_agreement_request() -> Weight { - // Proof Size summary in bytes: - // Measured: `380` - // Estimated: `3622` - // Minimum execution time: 28_813_000 picoseconds. - Weight::from_parts(30_137_000, 0) - .saturating_add(Weight::from_parts(0, 3622)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(2)) + // Measured: `734` + // Estimated: `4199` + // Minimum execution time: 43_000_000 picoseconds. + Weight::from_parts(44_000_000, 0) + .saturating_add(Weight::from_parts(0, 4199)) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) } /// Storage: `StorageProvider::Providers` (r:1 w:1) /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) @@ -362,8 +280,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `639` // Estimated: `3825` - // Minimum execution time: 35_490_000 picoseconds. - Weight::from_parts(37_387_000, 0) + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(19_000_000, 0) .saturating_add(Weight::from_parts(0, 3825)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(3)) @@ -378,8 +296,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `742` // Estimated: `6196` - // Minimum execution time: 81_582_000 picoseconds. - Weight::from_parts(84_313_000, 0) + // Minimum execution time: 44_000_000 picoseconds. + Weight::from_parts(47_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) @@ -397,11 +315,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1050 + a * (103 ±0)` // Estimated: `6196 + a * (2603 ±0)` - // Minimum execution time: 82_060_000 picoseconds. - Weight::from_parts(85_350_580, 0) + // Minimum execution time: 42_000_000 picoseconds. + Weight::from_parts(44_591_836, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 142_504 - .saturating_add(Weight::from_parts(35_766_869, 0).saturating_mul(a.into())) + // Standard Error: 258_954 + .saturating_add(Weight::from_parts(20_508_163, 0).saturating_mul(a.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) .saturating_add(T::DbWeight::get().writes(5)) @@ -420,8 +338,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1050` // Estimated: `6196` - // Minimum execution time: 79_695_000 picoseconds. - Weight::from_parts(83_280_000, 0) + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(44_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(5)) @@ -434,8 +352,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1697` // Estimated: `15165` - // Minimum execution time: 259_744_000 picoseconds. - Weight::from_parts(269_649_000, 0) + // Minimum execution time: 121_000_000 picoseconds. + Weight::from_parts(124_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -448,8 +366,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1751` // Estimated: `15165` - // Minimum execution time: 259_439_000 picoseconds. - Weight::from_parts(268_007_000, 0) + // Minimum execution time: 121_000_000 picoseconds. + Weight::from_parts(125_000_000, 0) .saturating_add(Weight::from_parts(0, 15165)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(1)) @@ -462,11 +380,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointPool` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) fn fund_checkpoint_pool() -> Weight { // Proof Size summary in bytes: - // Measured: `324` - // Estimated: `3789` - // Minimum execution time: 30_600_000 picoseconds. - Weight::from_parts(31_971_000, 0) - .saturating_add(Weight::from_parts(0, 3789)) + // Measured: `253` + // Estimated: `3718` + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(16_000_000, 0) + .saturating_add(Weight::from_parts(0, 3718)) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -485,13 +403,13 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// The range of component `s` is `[1, 5]`. fn provider_checkpoint(s: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `564 + s * (258 ±0)` - // Estimated: `4029 + s * (2835 ±0)` - // Minimum execution time: 83_843_000 picoseconds. - Weight::from_parts(36_491_057, 0) - .saturating_add(Weight::from_parts(0, 4029)) - // Standard Error: 75_016 - .saturating_add(Weight::from_parts(50_708_947, 0).saturating_mul(s.into())) + // Measured: `493 + s * (258 ±0)` + // Estimated: `3958 + s * (2835 ±0)` + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(7_075_759, 0) + .saturating_add(Weight::from_parts(0, 3958)) + // Standard Error: 361_690 + .saturating_add(Weight::from_parts(30_789_778, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(s.into()))) .saturating_add(T::DbWeight::get().writes(4)) @@ -503,11 +421,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::CheckpointConfigs` (`max_values`: None, `max_size`: Some(33), added: 2508, mode: `MaxEncodedLen`) fn configure_checkpoint_window() -> Weight { // Proof Size summary in bytes: - // Measured: `429` - // Estimated: `3894` - // Minimum execution time: 13_563_000 picoseconds. - Weight::from_parts(14_494_000, 0) - .saturating_add(Weight::from_parts(0, 3894)) + // Measured: `358` + // Estimated: `3823` + // Minimum execution time: 5_000_000 picoseconds. + Weight::from_parts(7_000_000, 0) + .saturating_add(Weight::from_parts(0, 3823)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } @@ -525,8 +443,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `929` // Estimated: `6196` - // Minimum execution time: 60_187_000 picoseconds. - Weight::from_parts(62_628_000, 0) + // Minimum execution time: 33_000_000 picoseconds. + Weight::from_parts(35_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -539,8 +457,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `397` // Estimated: `3593` - // Minimum execution time: 28_789_000 picoseconds. - Weight::from_parts(30_419_000, 0) + // Minimum execution time: 16_000_000 picoseconds. + Weight::from_parts(21_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) @@ -555,11 +473,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_checkpoint() -> Weight { // Proof Size summary in bytes: - // Measured: `893` - // Estimated: `4358` - // Minimum execution time: 36_317_000 picoseconds. - Weight::from_parts(38_019_000, 0) - .saturating_add(Weight::from_parts(0, 4358)) + // Measured: `855` + // Estimated: `4320` + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) + .saturating_add(Weight::from_parts(0, 4320)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -575,11 +493,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Challenges` (`max_values`: None, `max_size`: None, mode: `Measured`) fn challenge_off_chain() -> Weight { // Proof Size summary in bytes: - // Measured: `667` - // Estimated: `4132` - // Minimum execution time: 86_921_000 picoseconds. - Weight::from_parts(90_821_000, 0) - .saturating_add(Weight::from_parts(0, 4132)) + // Measured: `629` + // Estimated: `4094` + // Minimum execution time: 41_000_000 picoseconds. + Weight::from_parts(44_000_000, 0) + .saturating_add(Weight::from_parts(0, 4094)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -593,11 +511,11 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `StorageProvider::Providers` (`max_values`: None, `max_size`: Some(360), added: 2835, mode: `MaxEncodedLen`) fn challenge_replica() -> Weight { // Proof Size summary in bytes: - // Measured: `755` - // Estimated: `4220` - // Minimum execution time: 38_060_000 picoseconds. - Weight::from_parts(40_183_000, 0) - .saturating_add(Weight::from_parts(0, 4220)) + // Measured: `788` + // Estimated: `4253` + // Minimum execution time: 18_000_000 picoseconds. + Weight::from_parts(20_000_000, 0) + .saturating_add(Weight::from_parts(0, 4253)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -613,8 +531,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1093` // Estimated: `6196` - // Minimum execution time: 1_131_965_000 picoseconds. - Weight::from_parts(1_150_288_000, 0) + // Minimum execution time: 407_000_000 picoseconds. + Weight::from_parts(434_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -631,8 +549,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1306` // Estimated: `6660` - // Minimum execution time: 112_289_000 picoseconds. - Weight::from_parts(116_895_000, 0) + // Minimum execution time: 53_000_000 picoseconds. + Weight::from_parts(57_000_000, 0) .saturating_add(Weight::from_parts(0, 6660)) .saturating_add(T::DbWeight::get().reads(6)) .saturating_add(T::DbWeight::get().writes(4)) @@ -649,8 +567,8 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `1147` // Estimated: `6196` - // Minimum execution time: 58_417_000 picoseconds. - Weight::from_parts(60_959_000, 0) + // Minimum execution time: 28_000_000 picoseconds. + Weight::from_parts(31_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) @@ -663,10 +581,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) fn confirm_replica_sync() -> Weight { // Proof Size summary in bytes: - // Measured: `1008` + // Measured: `969` // Estimated: `6196` - // Minimum execution time: 71_815_000 picoseconds. - Weight::from_parts(74_641_000, 0) + // Minimum execution time: 38_000_000 picoseconds. + Weight::from_parts(42_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) @@ -679,10 +597,10 @@ impl pallet_storage_provider::WeightInfo for WeightInfo // Proof Size summary in bytes: // Measured: `506` // Estimated: `3692` - // Minimum execution time: 29_413_000 picoseconds. - Weight::from_parts(31_163_000, 0) + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(16_000_000, 0) .saturating_add(Weight::from_parts(0, 3692)) .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(2)) } -} +} \ No newline at end of file From 48a0a6628f44e7eea0ffd01897cb5f871bf4722a Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:15:43 +0700 Subject: [PATCH 29/44] feat(examples/papi): port smart-contract demos to the signed-terms flow The precompiles' bucket/drive creation selectors now redeem provider-signed AgreementTerms, so the PAPI demos follow: - sc-api.js: add h160ToSubstrate (AccountId32Mapper fallback account for unmapped H160s, i.e. deployed contracts) and negotiatePrecompileTerms (POST /negotiate shaped for the PrimitiveAgreementTerms ABI struct) - sc-coverage.js: createBucket/createBucketWithStorage and requestPrimaryAgreement + accept_agreement replaced by establishStorageAgreement; createDrive takes (name, provider, terms, sig); renumbered to the 13 remaining selectors - sc-flow.js / sc-team-drive.js / sc-token-gated.js: negotiate terms after deploy with the contract's substrate-mapped account as terms.owner (the contract is the precompile caller), then pass the signed bundle through buyStorage / createTeam / initialize --- examples/papi/sc-api.js | 52 +++++++++++ examples/papi/sc-coverage.js | 156 +++++++++++++++----------------- examples/papi/sc-flow.js | 33 ++++--- examples/papi/sc-team-drive.js | 20 ++-- examples/papi/sc-token-gated.js | 21 ++++- 5 files changed, 177 insertions(+), 105 deletions(-) diff --git a/examples/papi/sc-api.js b/examples/papi/sc-api.js index 39fe13eb..2aebdc46 100644 --- a/examples/papi/sc-api.js +++ b/examples/papi/sc-api.js @@ -11,8 +11,10 @@ */ import { Binary } from "@polkadot-api/substrate-bindings"; +import { ss58Address } from "@polkadot-labs/hdkd-helpers"; import { decodeEventLog, encodeFunctionData, keccak256 } from "viem"; +import { negotiateTerms } from "./api.js"; import { hexToBytes, requireOneEvent, submitTx, toHex } from "./common.js"; /** @@ -27,6 +29,56 @@ export function substrateToH160(publicKey) { return "0x" + Buffer.from(hash.slice(12)).toString("hex"); } +/** + * Substrate account `AccountId32Mapper` assigns to an unmapped H160 (e.g. a + * deployed contract): the 20 address bytes followed by 12 bytes of `0xEE`. + * Returns a signer-shaped `{ publicKey, address }` so it can be passed as + * the `owner` of negotiated agreement terms. + */ +export function h160ToSubstrate(addressBytes) { + const publicKey = new Uint8Array(32).fill(0xee); + publicKey.set(addressBytes, 0); + return { publicKey, address: ss58Address(publicKey) }; +} + +/** + * POST /negotiate on the provider node and shape the signed bundle for the + * precompiles' `PrimitiveAgreementTerms` ABI struct. `owner` must be the + * account the pallet will see as origin — the EOA's substrate account for + * direct precompile calls, or the contract's substrate-mapped account + * (`h160ToSubstrate(deployed.addressBytes)`) when a contract forwards the + * terms. + */ +export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, duration }) { + const signed = await negotiateTerms(providerUrl, { + owner: owner.address, + max_bytes: Number(maxBytes), + duration, + price_per_byte: 0, + replica_params: null, + }); + const t = signed.terms; + const rp = t.replica_params; + return { + terms: { + owner: toHex(owner.publicKey), + maxBytes: BigInt(t.max_bytes), + duration: Number(t.duration), + pricePerByte: BigInt(t.price_per_byte), + validUntil: Number(t.valid_until), + nonce: BigInt(t.nonce), + hasReplicaParams: rp != null, + replicaParams: { + syncBalance: BigInt(rp?.sync_balance ?? 0), + minSyncInterval: Number(rp?.min_sync_interval ?? 0), + }, + }, + // Hex SCALE-encoded MultiSignature (variant byte + raw sig) — passed + // through verbatim as the `bytes signature` ABI param. + signature: signed.signature, + }; +} + /** * Generous defaults — pallet_revive bounds these at the runtime config level * (`RuntimeMemory`, `PVFMemory`). Picking large values means the dispatch diff --git a/examples/papi/sc-coverage.js b/examples/papi/sc-coverage.js index 6329dd29..a2b05fcc 100644 --- a/examples/papi/sc-coverage.js +++ b/examples/papi/sc-coverage.js @@ -6,10 +6,11 @@ * * Each selector gets one happy-path invocation, and the script asserts the * pallet's storage or events were updated as expected. Preconditions - * (bucket existence, accepted agreement, checkpoint) are chained where - * necessary; provider-side ops (`accept_agreement`, `respond_to_challenge`, - * `submitClientCheckpoint`) stay on the substrate side via existing - * `api.js` helpers since the precompile only covers the client surface. + * (bucket existence, established agreement, checkpoint) are chained where + * necessary; provider-side ops (`POST /negotiate`, `respond_to_challenge`, + * `submitClientCheckpoint`) stay off-chain / on the substrate side via + * existing `api.js` helpers since the precompile only covers the client + * surface. * * Prerequisites: * - Chain at ws://127.0.0.1:2222 with pallet_revive wired in. @@ -24,7 +25,6 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { - acceptAgreement, fetchCheckpointSignature, respondToChallenge, submitClientCheckpoint, @@ -43,7 +43,12 @@ import { waitForChainReady, waitForNextBlock, } from "./common.js"; -import { callContract, encodeCall, ensureAccountMapped } from "./sc-api.js"; +import { + callContract, + encodeCall, + ensureAccountMapped, + negotiatePrecompileTerms, +} from "./sc-api.js"; const { chainWs, providerUrl, providerSeed, clientSeed } = parseProviderClientArgs(); @@ -53,14 +58,16 @@ const CONTRACT_JSON = resolve(HERE, "../contracts/build/combined.json"); const WEB3_STORAGE_ADDR = hexToBytes("0x0000000000000000000000000000000009010000"); const DRIVE_REGISTRY_ADDR = hexToBytes("0x0000000000000000000000000000000009020000"); -const UNIT = 10n ** 12n; - /** Send raw calldata to a precompile address as a signed substrate tx. */ async function callPrecompile(api, signer, addr, abi, fnName, args, opts = {}) { const data = encodeCall(abi, fnName, args); return callContract(api, signer, addr, data, opts); } +/** Negotiate terms for a direct precompile call signed by `owner`. */ +const negotiateAbiTerms = (owner, req) => + negotiatePrecompileTerms(providerUrl, owner, req); + /** Assert an event of the named pallet was emitted in this tx. */ function assertEvent(events, type, valueType, label) { const ev = events.find( @@ -97,9 +104,8 @@ async function main() { // -------- Setup -------------------------------------------------------- // Silence any other dev-key providers (Charlie/Ferdie may have been - // registered by earlier demos in the CI matrix) so create_bucket_with_storage - // auto-matching is deterministic and the substrate-side challenge lookups - // at (bucket_id, provider) hit the agreement we just created. + // registered by earlier demos in the CI matrix) so only the provider we + // negotiate with holds agreements during this run. console.log("\n[setup] provider + account mapping…"); await ensureProviderRegistered(api, provider, providerUrl, { pricePerByte: 1n, @@ -116,11 +122,22 @@ async function main() { // Storage-provider precompile (0x…09010000) // ==================================================================== - // 1. createBucket ----------------------------------------------------- - console.log("\n[1] IWeb3Storage.createBucket(1)"); + // 1. establishStorageAgreement ---------------------------------------- + // Negotiated terms create the bucket + primary agreement atomically, so + // the agreement is already active for the top-up/extend/end steps below. + console.log("\n[1] IWeb3Storage.establishStorageAgreement(provider, terms[2KiB×100], sig)"); + const maxBytesA = 2048n; + const durationA = 100; + const maxPaymentA = maxBytesA * BigInt(durationA) * 10n; // generous + const signedA = await negotiateAbiTerms(client, { maxBytes: maxBytesA, duration: durationA }); let nextBucketBefore = await api.query.StorageProvider.NextBucketId.getValue(); - let r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "createBucket", [1]); - const created = assertEvent(r.events, "StorageProvider", "BucketCreated", "createBucket"); + let r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "establishStorageAgreement", [ + toHex(providerBytes32), + signedA.terms, + signedA.signature, + ]); + const created = assertEvent(r.events, "StorageProvider", "BucketCreated", "establishStorageAgreement"); + assertEvent(r.events, "StorageProvider", "StorageAgreementEstablished", "establishStorageAgreement"); const bucketA = created.bucket_id; assert.strictEqual(bucketA, nextBucketBefore, "BucketCreated.bucket_id == pre-call NextBucketId"); console.log(" bucketA =", bucketA.toString()); @@ -147,27 +164,8 @@ async function main() { ]); assertEvent(r.events, "StorageProvider", "MemberRemoved", "removeMember"); - // 4. requestPrimaryAgreement ------------------------------------------ - console.log("\n[4] IWeb3Storage.requestPrimaryAgreement(bucketA, provider, …)"); - const maxBytesA = 2048n; - const durationA = 100; - const maxPaymentA = maxBytesA * BigInt(durationA) * 10n; // generous - r = await callPrecompile( - api, - client, - WEB3_STORAGE_ADDR, - iWeb3, - "requestPrimaryAgreement", - [bucketA, toHex(providerBytes32), maxBytesA, durationA, maxPaymentA] - ); - assertEvent(r.events, "StorageProvider", "AgreementRequested", "requestPrimaryAgreement"); - - // Provider-side accept (substrate-native). - console.log(" [substrate] acceptAgreement"); - await acceptAgreement(api, provider, bucketA); - - // 5. topUpAgreement --------------------------------------------------- - console.log("\n[5] IWeb3Storage.topUpAgreement(bucketA, provider, +1024 bytes, …)"); + // 4. topUpAgreement --------------------------------------------------- + console.log("\n[4] IWeb3Storage.topUpAgreement(bucketA, provider, +1024 bytes, …)"); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "topUpAgreement", [ bucketA, toHex(providerBytes32), @@ -176,8 +174,8 @@ async function main() { ]); assertEvent(r.events, "StorageProvider", "AgreementToppedUp", "topUpAgreement"); - // 6. extendAgreement -------------------------------------------------- - console.log("\n[6] IWeb3Storage.extendAgreement(bucketA, provider, +50 blocks, …)"); + // 5. extendAgreement -------------------------------------------------- + console.log("\n[5] IWeb3Storage.extendAgreement(bucketA, provider, +50 blocks, …)"); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "extendAgreement", [ bucketA, toHex(providerBytes32), @@ -186,38 +184,35 @@ async function main() { ]); assertEvent(r.events, "StorageProvider", "AgreementExtended", "extendAgreement"); - // 7. endAgreementPay -------------------------------------------------- - console.log("\n[7] IWeb3Storage.endAgreementPay(bucketA, provider)"); + // 6. endAgreementPay -------------------------------------------------- + console.log("\n[6] IWeb3Storage.endAgreementPay(bucketA, provider)"); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "endAgreementPay", [ bucketA, toHex(providerBytes32), ]); assertEvent(r.events, "StorageProvider", "AgreementEnded", "endAgreementPay"); - // 8. createBucketWithStorage (large — for endAgreementBurn) ---------- + // 7. establishStorageAgreement (large — for endAgreementBurn) -------- // Burn-percent transfers send to the treasury account; the transfer uses // `KeepAlive`, so the burned amount must be ≥ ExistentialDeposit (1 // MILLIUNIT = 1e9 atomic). 10% of `1MiB × 100k blocks × 1` ≈ 1e10 atomic, // comfortably above ED. - console.log("\n[8] IWeb3Storage.createBucketWithStorage(1MiB, 100k blocks, maxPrice=10) [burn-sized]"); + console.log("\n[7] IWeb3Storage.establishStorageAgreement(provider, terms[1MiB×100k], sig) [burn-sized]"); + const signedB = await negotiateAbiTerms(client, { maxBytes: 1n << 20n, duration: 100_000 }); nextBucketBefore = await api.query.StorageProvider.NextBucketId.getValue(); - r = await callPrecompile( - api, - client, - WEB3_STORAGE_ADDR, - iWeb3, - "createBucketWithStorage", - [1n << 20n, 100_000, 10n], - { value: 200n * UNIT } // contract balance for payment reserve - ); - const createdB = assertEvent(r.events, "StorageProvider", "BucketCreated", "createBucketWithStorage"); + r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "establishStorageAgreement", [ + toHex(providerBytes32), + signedB.terms, + signedB.signature, + ]); + const createdB = assertEvent(r.events, "StorageProvider", "BucketCreated", "establishStorageAgreement"); const bucketB = createdB.bucket_id; assert.strictEqual(bucketB, nextBucketBefore); - assertEvent(r.events, "StorageProvider", "AgreementAccepted", "createBucketWithStorage"); + assertEvent(r.events, "StorageProvider", "StorageAgreementEstablished", "establishStorageAgreement"); console.log(" bucketB =", bucketB.toString()); - // 9. endAgreementBurn (early-terminate bucketB) ----------------------- - console.log("\n[9] IWeb3Storage.endAgreementBurn(bucketB, provider, burn=10%)"); + // 8. endAgreementBurn (early-terminate bucketB) ----------------------- + console.log("\n[8] IWeb3Storage.endAgreementBurn(bucketB, provider, burn=10%)"); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "endAgreementBurn", [ bucketB, toHex(providerBytes32), @@ -225,22 +220,19 @@ async function main() { ]); assertEvent(r.events, "StorageProvider", "AgreementEnded", "endAgreementBurn"); - // 10. challengeCheckpoint + freezeBucket on a fresh small bucket ------ + // 9. challengeCheckpoint + freezeBucket on a fresh small bucket ------- // Upload + checkpoint give us both a snapshot to freeze and a leaf to // challenge. The agreement is left open and is not ended; settlement // happens through chain-driven expiry, not this test. - console.log("\n[10] IWeb3Storage.createBucketWithStorage(2KiB, 100, maxPrice=10) [freeze/challenge target]"); + console.log("\n[9] IWeb3Storage.establishStorageAgreement(provider, terms[2KiB×100], sig) [freeze/challenge target]"); + const signedC = await negotiateAbiTerms(client, { maxBytes: 2048n, duration: 100 }); nextBucketBefore = await api.query.StorageProvider.NextBucketId.getValue(); - r = await callPrecompile( - api, - client, - WEB3_STORAGE_ADDR, - iWeb3, - "createBucketWithStorage", - [2048n, 100, 10n], - { value: 5n * UNIT } - ); - const createdC = assertEvent(r.events, "StorageProvider", "BucketCreated", "createBucketWithStorage"); + r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "establishStorageAgreement", [ + toHex(providerBytes32), + signedC.terms, + signedC.signature, + ]); + const createdC = assertEvent(r.events, "StorageProvider", "BucketCreated", "establishStorageAgreement"); const bucketC = createdC.bucket_id; assert.strictEqual(bucketC, nextBucketBefore); console.log(" bucketC =", bucketC.toString()); @@ -250,7 +242,7 @@ async function main() { const ck = await fetchCheckpointSignature(providerUrl, bucketC); await submitClientCheckpoint(api, client, provider, bucketC, ck); - console.log("\n[10a] IWeb3Storage.challengeCheckpoint(bucketC, provider, leafIdx, chunkIdx=0)"); + console.log("\n[9a] IWeb3Storage.challengeCheckpoint(bucketC, provider, leafIdx, chunkIdx=0)"); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "challengeCheckpoint", [ bucketC, toHex(providerBytes32), @@ -265,7 +257,7 @@ async function main() { ); await respondToChallenge(api, provider, challenge.challenge_id, proof); - console.log("\n[11] IWeb3Storage.freezeBucket(bucketC)"); + console.log("\n[10] IWeb3Storage.freezeBucket(bucketC)"); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "freezeBucket", [bucketC]); assertEvent(r.events, "StorageProvider", "BucketFrozen", "freezeBucket"); @@ -273,23 +265,23 @@ async function main() { // Drive-registry precompile (0x…09020000) // ==================================================================== - // 12. createDrive ----------------------------------------------------- - console.log("\n[12] IDriveRegistry.createDrive(\"cov\", 1MiB, 50 blocks, 1 UNIT, default-providers)"); + // 11. createDrive ----------------------------------------------------- + console.log("\n[11] IDriveRegistry.createDrive(\"cov\", provider, terms[1MiB×50], sig)"); + const signedD = await negotiateAbiTerms(client, { maxBytes: 1n << 20n, duration: 50 }); const nextDriveBefore = await api.query.DriveRegistry.NextDriveId.getValue(); r = await callPrecompile(api, client, DRIVE_REGISTRY_ADDR, iDrive, "createDrive", [ "cov", - 1n << 20n, // 1 MiB - 50, // storagePeriod blocks - UNIT, // payment - 0, // minProviders=0 → None (use runtime default) + toHex(providerBytes32), + signedD.terms, + signedD.signature, ]); const driveEvt = assertEvent(r.events, "DriveRegistry", "DriveCreated", "createDrive"); const driveId = driveEvt.drive_id; assert.strictEqual(driveId, nextDriveBefore); console.log(" driveId =", driveId.toString()); - // 13. shareDrive ------------------------------------------------------ - console.log("\n[13] IDriveRegistry.shareDrive(driveId, Charlie, Reader)"); + // 12. shareDrive ------------------------------------------------------ + console.log("\n[12] IDriveRegistry.shareDrive(driveId, Charlie, Reader)"); r = await callPrecompile(api, client, DRIVE_REGISTRY_ADDR, iDrive, "shareDrive", [ driveId, toHex(memberBytes32), @@ -297,22 +289,22 @@ async function main() { ]); assertEvent(r.events, "DriveRegistry", "DriveShared", "shareDrive"); - // 14. unshareDrive ---------------------------------------------------- - console.log("\n[14] IDriveRegistry.unshareDrive(driveId, Charlie)"); + // 13. unshareDrive ---------------------------------------------------- + console.log("\n[13] IDriveRegistry.unshareDrive(driveId, Charlie)"); r = await callPrecompile(api, client, DRIVE_REGISTRY_ADDR, iDrive, "unshareDrive", [ driveId, toHex(memberBytes32), ]); assertEvent(r.events, "DriveRegistry", "DriveUnshared", "unshareDrive"); - // 15. deleteDrive ----------------------------------------------------- - console.log("\n[15] IDriveRegistry.deleteDrive(driveId)"); + // 14. deleteDrive ----------------------------------------------------- + console.log("\n[14] IDriveRegistry.deleteDrive(driveId)"); r = await callPrecompile(api, client, DRIVE_REGISTRY_ADDR, iDrive, "deleteDrive", [ driveId, ]); assertEvent(r.events, "DriveRegistry", "DriveDeleted", "deleteDrive"); - console.log("\n✅ All 15 selectors exercised, every expected event observed"); + console.log("\n✅ All 13 selectors exercised, every expected event observed"); } finally { papi.destroy(); } diff --git a/examples/papi/sc-flow.js b/examples/papi/sc-flow.js index 113701e6..2b00dfd3 100644 --- a/examples/papi/sc-flow.js +++ b/examples/papi/sc-flow.js @@ -51,6 +51,8 @@ import { deployContract, encodeCall, ensureAccountMapped, + h160ToSubstrate, + negotiatePrecompileTerms, } from "./sc-api.js"; const { @@ -68,7 +70,6 @@ const CONTRACT_KEY = "StorageMarketplace.sol:StorageMarketplace"; // the deployer's funded balance, large enough to exercise upload + challenge. const MAX_BYTES = 1024n; // 1 KiB const DURATION = 50; // blocks -const MAX_PRICE_PER_BYTE = 100n; // upper bound; provider's actual is 1 const UNIT = 10n ** 12n; // matches runtime constant const FUND_VALUE = 5n * UNIT; // contract gets 5 UNIT; payment ~ 51200 atomic, vast headroom @@ -87,15 +88,12 @@ async function main() { const provider = makeSigner(PROVIDER_SEED); const client = makeSigner(CLIENT_SEED); - // 1) Provider setup. Sets accepting_primary so create_bucket_with_storage - // can auto-match, and pricePerByte=1 so payment math is trivial. We - // also have to register both the deployer and caller with pallet_revive + // 1) Provider setup. Sets accepting_primary + pricePerByte=1 so payment + // math is trivial, and silences every other dev-key provider so only + // the provider we negotiate with holds agreements. We also have to + // register both the deployer and caller with pallet_revive // (`map_account`) — substrate-native accounts can't dispatch contract // calls or be the target of value transfers until they're mapped. - // 1) Provider setup. Sets accepting_primary so create_bucket_with_storage - // can auto-match, and silences every other dev-key provider so the - // auto-match is deterministic (in CI other demos register //Charlie - // and //Ferdie before this script runs). console.log("\n[1/6] Provider setup + Revive account mapping…"); await ensureProviderRegistered(api, provider, PROVIDER_URL, { pricePerByte: 1n, @@ -125,13 +123,20 @@ async function main() { const deployed = await deployContract(api, provider, bytecode); console.log(" contract:", deployed.address); - // 4) //Bob buys storage. msg.value funds the contract's substrate account; - // the precompile then reserves the agreement payment from that balance. - console.log("\n[4/6] buyStorage{value: 5 UNIT}(maxBytes=1KiB, duration=50, …)…"); + // 4) //Bob buys storage. The terms are negotiated off-chain with the + // provider using the *contract's* substrate-mapped account as owner + // (the contract is the precompile caller); msg.value funds that + // account so the precompile can reserve the agreement payment. + console.log("\n[4/6] buyStorage{value: 5 UNIT}(provider, terms[1KiB×50], sig)…"); + const contractAccount = h160ToSubstrate(deployed.addressBytes); + const signed = await negotiatePrecompileTerms(PROVIDER_URL, contractAccount, { + maxBytes: MAX_BYTES, + duration: DURATION, + }); const buyData = encodeCall(abi, "buyStorage", [ - MAX_BYTES, - DURATION, - MAX_PRICE_PER_BYTE, + toHex(provider.publicKey), + signed.terms, + signed.signature, ]); const buyResult = await callContract(api, client, deployed.addressBytes, buyData, { value: FUND_VALUE, diff --git a/examples/papi/sc-team-drive.js b/examples/papi/sc-team-drive.js index a8aadfb1..a42ac28c 100644 --- a/examples/papi/sc-team-drive.js +++ b/examples/papi/sc-team-drive.js @@ -42,6 +42,8 @@ import { deployContract, encodeCall, ensureAccountMapped, + h160ToSubstrate, + negotiatePrecompileTerms, } from "./sc-api.js"; const { chainWs, providerUrl, providerSeed, clientSeed } = parseProviderClientArgs(); @@ -96,14 +98,20 @@ async function main() { const deployed = await deployContract(api, client, bytecode); console.log(" contract:", deployed.address); - // 2) createTeam{value: 10 UNIT} — the contract becomes the drive owner. - console.log("\n[2/4] createTeam{value: 10 UNIT}('team-cov', 1MiB, 50 blocks, …)"); + // 2) createTeam{value: 10 UNIT} — the contract becomes the drive owner, + // so the terms are negotiated with the contract's substrate-mapped + // account as owner; msg.value funds that account's payment reserve. + console.log("\n[2/4] createTeam{value: 10 UNIT}('team-cov', provider, terms[1MiB×50], sig)"); + const contractAccount = h160ToSubstrate(deployed.addressBytes); + const signed = await negotiatePrecompileTerms(providerUrl, contractAccount, { + maxBytes: 1n << 20n, // 1 MiB capacity + duration: 50, + }); const createData = encodeCall(abi, "createTeam", [ "team-cov", - 1n << 20n, // 1 MiB capacity - 50, - UNIT, // payment to lock - 0, // minProviders=0 → None + toHex(provider.publicKey), + signed.terms, + signed.signature, ]); let r = await callContract(api, client, deployed.addressBytes, createData, { value: 10n * UNIT, diff --git a/examples/papi/sc-token-gated.js b/examples/papi/sc-token-gated.js index fdc1c212..3ed62da5 100644 --- a/examples/papi/sc-token-gated.js +++ b/examples/papi/sc-token-gated.js @@ -33,6 +33,7 @@ import { makeSigner, parseProviderClientArgs, requireOneEvent, + toHex, waitForBlockProduction, waitForChainReady, waitForNextBlock, @@ -43,6 +44,8 @@ import { deployContract, encodeCall, ensureAccountMapped, + h160ToSubstrate, + negotiatePrecompileTerms, substrateToH160, } from "./sc-api.js"; @@ -102,14 +105,26 @@ async function main() { const deployed = await deployContract(api, publisher, bytecode); console.log(" contract:", deployed.address); - // 2) initialize{value: 5 UNIT}('cov-bucket-N', …). + // 2) initialize{value: 5 UNIT}('cov-bucket-N', …) — terms negotiated with + // the contract's substrate-mapped account as owner; msg.value funds that + // account's payment reserve. // Bucket name: 3-63 chars, lowercase alphanumeric + hyphens. Append the // block number so the name is unique across reruns on the same chain // (`pallet_s3_registry` enforces global name uniqueness). const blockHead = await api.query.System.Number.getValue(); const bucketName = `cov-bucket-${Number(blockHead)}`; - console.log(`\n[2/6] initialize{value: 5 UNIT}('${bucketName}', 1MiB, 50 blocks, …)`); - const initData = encodeCall(abi, "initialize", [bucketName, 1n << 20n, 50, 2n * UNIT]); + console.log(`\n[2/6] initialize{value: 5 UNIT}('${bucketName}', provider, terms[1MiB×50], sig)`); + const contractAccount = h160ToSubstrate(deployed.addressBytes); + const signed = await negotiatePrecompileTerms(providerUrl, contractAccount, { + maxBytes: 1n << 20n, + duration: 50, + }); + const initData = encodeCall(abi, "initialize", [ + bucketName, + toHex(provider.publicKey), + signed.terms, + signed.signature, + ]); let r = await callContract(api, publisher, deployed.addressBytes, initData, { value: 5n * UNIT, }); From 1266734de13214069711ff702826dc5273b9d8a7 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:41:00 +0700 Subject: [PATCH 30/44] chore: make all tests passed --- client/src/agreement.rs | 5 +- runtime/src/revive.rs | 1 + runtimes/web3-storage-paseo/src/revive.rs | 1 + runtimes/web3-storage-paseo/tests/tests.rs | 1 + .../src/components/NewDriveDialog.tsx | 60 +++++++++++++++--- .../drive-ui/src/lib/drive-client.ts | 5 +- .../drive-ui/src/state/drive.state.ts | 56 +++------------- user-interfaces/pnpm-lock.yaml | 27 -------- .../papi/.papi/descriptors/package.json | 2 +- .../papi/.papi/metadata/parachain.scale | Bin 186426 -> 225410 bytes .../shared/papi/.papi/polkadot-api.json | 4 +- .../shared/test-helpers/src/buckets.ts | 5 +- 12 files changed, 72 insertions(+), 95 deletions(-) diff --git a/client/src/agreement.rs b/client/src/agreement.rs index 4de114c7..ebe45204 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -15,7 +15,7 @@ //! encoding has to be used on both sides — `sign_terms` enforces that. use codec::Encode; -use serde::{Deserialize, Serialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize}; use sp_core::hashing::blake2_256; use sp_runtime::{AccountId32, MultiSignature}; use storage_primitives::AgreementTerms; @@ -97,7 +97,6 @@ mod hex_multi_signature { } } - // Universal helper function to accept either a JSON string or raw JSON number fn deserialize_number_from_string_or_number<'de, T, D>(deserializer: D) -> Result where @@ -116,4 +115,4 @@ where StringOrNumber::String(s) => s.parse::().map_err(serde::de::Error::custom), StringOrNumber::Number(n) => Ok(n), } -} \ No newline at end of file +} diff --git a/runtime/src/revive.rs b/runtime/src/revive.rs index f301e525..790c2ed1 100644 --- a/runtime/src/revive.rs +++ b/runtime/src/revive.rs @@ -79,6 +79,7 @@ impl EthExtra for EthExtraImpl { frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + frame_metadata_hash_extension::CheckMetadataHash::::new(true), pallet_revive::evm::tx_extension::SetOrigin::::new_from_eth_transaction(), ) .into() diff --git a/runtimes/web3-storage-paseo/src/revive.rs b/runtimes/web3-storage-paseo/src/revive.rs index 976e594d..1668b21a 100644 --- a/runtimes/web3-storage-paseo/src/revive.rs +++ b/runtimes/web3-storage-paseo/src/revive.rs @@ -79,6 +79,7 @@ impl EthExtra for EthExtraImpl { frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + frame_metadata_hash_extension::CheckMetadataHash::::new(true), pallet_revive::evm::tx_extension::SetOrigin::::new_from_eth_transaction(), ) .into() diff --git a/runtimes/web3-storage-paseo/tests/tests.rs b/runtimes/web3-storage-paseo/tests/tests.rs index 5b783bda..a1393910 100644 --- a/runtimes/web3-storage-paseo/tests/tests.rs +++ b/runtimes/web3-storage-paseo/tests/tests.rs @@ -63,6 +63,7 @@ fn construct_extrinsic( }), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(0u128), + frame_metadata_hash_extension::CheckMetadataHash::::new(true), // SetOrigin's substrate-signed path is a no-op; only the eth path // (built by `EthExtraImpl::get_eth_extension`) sets `is_eth_transaction`. pallet_revive::evm::tx_extension::SetOrigin::::default(), diff --git a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx index 66b107ba..fff5457d 100644 --- a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx +++ b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx @@ -13,11 +13,17 @@ import { canRetryCreation, createDrive, dismissCreation, + getSignerAddress, retryCreation, useCreations, type CreationStatus, } from "@/state"; -import type { AvailableProvider } from "@/lib/drive-client"; +import { + negotiateTerms, + parseMultiaddrToHttp, + type AvailableProvider, + type SignedTerms, +} from "@/lib/drive-client"; import { formatBytes } from "@/lib/utils"; import ProviderPickerPanel from "./ProviderPickerPanel"; @@ -61,7 +67,7 @@ function CreationStatusCard({

{item.name}

- {item.stage === "submitting" && "Negotiating with provider and submitting on-chain..."} + {item.stage === "submitting" && "Submitting on-chain..."} {item.stage === "ready" && "Drive is ready to use"} {item.stage === "failed" && (item.error || "Something went wrong")}

@@ -100,21 +106,53 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro const [duration, setDuration] = useState("10000"); const [pricePerByte, setPricePerByte] = useState("0"); const [submitting, setSubmitting] = useState(false); + const [negotiateError, setNegotiateError] = useState(null); /** - * Clicking a provider's Select button IS the submit action. Hand the - * form values + picked provider to the state hook, which runs - * `negotiate → submit_create_drive` atomically. + * Clicking a provider's Select button IS the submit action. Negotiate + * signed terms with the provider here, then hand them + the form values + * to the state hook, which runs the `submit_create_drive` chain submit. */ const handleProviderSelect = async (provider: AvailableProvider) => { setSubmitting(true); + setNegotiateError(null); try { + const url = parseMultiaddrToHttp(provider.multiaddr); + if (!url) { + setNegotiateError( + `Provider ${provider.account} has an unparseable multiaddr: ${provider.multiaddr}`, + ); + return; + } + + const owner = getSignerAddress(); + if (!owner) { + setNegotiateError("Signer not set"); + return; + } + + // Failure here means re-negotiate from scratch on retry. + let signed: SignedTerms; + try { + signed = await negotiateTerms(url, { + owner, + max_bytes: BigInt(capacity), + duration: parseInt(duration, 10), + price_per_byte: BigInt(pricePerByte || "0"), + replica_params: null, + }); + } catch (err) { + setNegotiateError( + err instanceof Error ? err.message : "Failed to negotiate with provider", + ); + return; + } + const drive = await createDrive({ name: name || undefined, - maxCapacity: BigInt(capacity), - storagePeriod: parseInt(duration, 10), - pricePerByte: BigInt(pricePerByte || "0"), provider, + url, + signed, }); if (drive) { setName(""); @@ -187,6 +225,12 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro disabled={submitting} /> + {negotiateError && ( +

+ {negotiateError} +

+ )} + {creations.length > 0 && (
{creations.map((item) => ( diff --git a/user-interfaces/drive-ui/src/lib/drive-client.ts b/user-interfaces/drive-ui/src/lib/drive-client.ts index 1891341f..8cb642ce 100644 --- a/user-interfaces/drive-ui/src/lib/drive-client.ts +++ b/user-interfaces/drive-ui/src/lib/drive-client.ts @@ -197,10 +197,7 @@ function hexToBytes(hex: string): Uint8Array { /** * Build the `{ provider, terms, sig }` args shared by every signed-terms - * extrinsic. The inner of `MultiSignature::Sr25519` is `[u8; 64]` which - * PAPI v2 encodes as `SizedBytes(64) = Codec` — pass a - * `0x`-prefixed hex string, NOT a `Uint8Array`. Mirrors - * `console-ui/src/lib/storage.ts::buildSignedTermsArgs`. + * extrinsic. */ export function buildSignedTermsArgs( providerAccount: string, diff --git a/user-interfaces/drive-ui/src/state/drive.state.ts b/user-interfaces/drive-ui/src/state/drive.state.ts index 2901818e..86d7fd6d 100644 --- a/user-interfaces/drive-ui/src/state/drive.state.ts +++ b/user-interfaces/drive-ui/src/state/drive.state.ts @@ -10,8 +10,6 @@ import { BehaviorSubject, combineLatest, distinctUntilChanged, Subscription } fr import { bind } from "@react-rxjs/core"; import { DriveClient, - negotiateTerms, - parseMultiaddrToHttp, type AvailableProvider, type DriveInfo, type FsEntry, @@ -37,11 +35,12 @@ export interface CreationStatus { export interface CreateDriveInput { name?: string; - maxCapacity: bigint; - storagePeriod: number; - pricePerByte?: bigint; /** The provider the user picked. */ provider: AvailableProvider; + /** Provider HTTP endpoint (parsed from its multiaddr). */ + url: string; + /** Terms already negotiated with the provider (`POST /negotiate`). */ + signed: SignedTerms; } export type ViewMode = "list" | "grid"; @@ -340,22 +339,6 @@ async function runChainSubmit(id: string, ctx: RetryCtx): Promise { if (!client.hasApi() || !client.hasSigner()) return null; - const url = parseMultiaddrToHttp(input.provider.multiaddr); - if (!url) { - const id = crypto.randomUUID(); - creations$.next([ - ...creations$.getValue(), - { - id, - name: input.name || "Untitled Drive", - stage: "failed", - elapsedMs: 0, - error: `Provider ${input.provider.account} has an unparseable multiaddr: ${input.provider.multiaddr}`, - }, - ]); - return null; - } - const id = crypto.randomUUID(); const displayName = input.name || "Untitled Drive"; creations$.next([ @@ -363,33 +346,10 @@ export async function createDrive(input: CreateDriveInput): Promise=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} @@ -1648,7 +1642,6 @@ packages: resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.61.0': resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} @@ -1659,7 +1652,6 @@ packages: resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.61.0': resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} @@ -1670,7 +1662,6 @@ packages: resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.61.0': resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} @@ -1681,7 +1672,6 @@ packages: resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-musl@4.61.0': resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} @@ -1692,7 +1682,6 @@ packages: resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-gnu@4.61.0': resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} @@ -1703,7 +1692,6 @@ packages: resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-musl@4.61.0': resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} @@ -1714,7 +1702,6 @@ packages: resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.61.0': resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} @@ -1725,7 +1712,6 @@ packages: resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-musl@4.61.0': resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} @@ -1736,7 +1722,6 @@ packages: resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.61.0': resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} @@ -1747,7 +1732,6 @@ packages: resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.61.0': resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} @@ -1758,7 +1742,6 @@ packages: resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.61.0': resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} @@ -1769,7 +1752,6 @@ packages: resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.0': resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} @@ -1780,7 +1762,6 @@ packages: resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-linux-x64-musl@4.61.0': resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} @@ -1931,28 +1912,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.4': resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.4': resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.4': resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.4': resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} @@ -2604,28 +2581,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} diff --git a/user-interfaces/shared/papi/.papi/descriptors/package.json b/user-interfaces/shared/papi/.papi/descriptors/package.json index 19497ba7..d60a187d 100644 --- a/user-interfaces/shared/papi/.papi/descriptors/package.json +++ b/user-interfaces/shared/papi/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.449225876343641783", + "version": "0.1.0-autogenerated.16173887892502261417", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/user-interfaces/shared/papi/.papi/metadata/parachain.scale b/user-interfaces/shared/papi/.papi/metadata/parachain.scale index 4d5747827fc1bebec04e31eacd51c816cc1b9f28..32480361c70c3d3030a5d537a9f25049446736d5 100644 GIT binary patch delta 45298 zcmdUY4|rU~b??mVm9%TiAb|uj*l-Ot$VOTu{0CXc#KKmv!2c~1TyUh7_DWhT?aKSd zw$jiWr{T43;v{^IbD@Q{ehp1XFo_c!K^iCc#ZGVnNoeB+x6m*2l~-uuq_p@I+R(n= zIWza}-IZ)ilJ~yv<#Uj9@12=5XU?2|bIzHseQElKU!GAl>f{qOi>Do`x>R1!lgnqa zi6N(VERl0E`Ba`{(omB*^4H*9ovq;@!#xHqzIgM9x*`p+hNVP2ITkr5}~o8>+l^j|eA@X)e)gk03(*7|2_4)>vYE zB$G%s$XRzd10997n}uj;mYsGsvD5ClV~y>9n#!b`?ELsxY5-l0+sRaJERi1=c9M4g zxSdGbUHM@r>$rtc`?gK%);4ckzrxNr*&R+cE?Ta(*C(Kn)=@=klZvtzWqy~n_afj6aw(oV8(Yg{y3ZLiJb9kD_745xB7!Giy! z)0w>8?*J$_fGU~FIs^HU@tf>KKJSc<*NX}c{`Q16ZT*t zHR2>U$k24-c6VOSA!jG>&)%6$jEy;2doYtN&KMI8wQ=M2`-Yvgoh$T@rZ8?2I2(4h z$gEyEdmxhrO7b~7MdJdT1ZJHZ>oxD&+lQRAlT8h@x33s>2DUlLu3h@`52$-b3BQcUnCR{yDATUF26e>YgV-`X%VsxO$yG_ zvgMY5OG^Df2;Db5~%LL_QHB zC%L>ru1Rcj2!p&{G;5%IU?2meG}##}O(xAm#V`*#S)N}mGwRs=8nkj6`W$G8CIbTM zOeV38IXg2*nBGhnMmGa_T5B3E{!4tzGd0v^x}^91c=lj6gJlHP`ZC$nP%7O=Z}kZA zX1QSBH;3z2X|&jk7LY86U;s3`lUCc=T_Kt6s7~S9K8gcvX`FGn5 zfEZ5XiGYcE2Zj^bA#~{j^BJ2bfb|71*fC6Z1f=PYrkX2^I?4DkLI<#JW|&Hk74kMc z&@F)-nk-uS$Z_Lh9w6&xocT*(V?!nE+-M@3Z}va*-!U0u7vwSnDQwXsdP+Ka7xg=j z>G|U+5n>`ePQ+LUUI%E-0@QjQ)@5i63FvXN&QM__0b2Iy;@tgoY4FpaCEWmvkji0W z5ePh0VhtJ&%!?Sr2$4yC7|8B(`C|v{G%?a^=((?{Rwl5)O2F`^Xb)r^p7%S?unF5W zpP;VuPQzI?S=X<(%Y>Xu_}}-F%Pw&z@3)#U`Cv60L;7Inv{-JeP)vg2{FSTfrkETji$(_-aZJC!6n zVyh96-PO0r85wME58O3BWc;-{0&$7yN%N1FnZiBjqXij9pxFT9NY*2G3&tyxX*L<<9{`1dfd@ zsyCaRa?`Bi^^fPBT>Oo9D?x}R11YzRa86tY0%&uNL>M$ALTADV=Ok2?9lVg(+kg=%l9`gX+eYl}fv&sxhQ&*!RBBx)xv+<*lc7XTtGmSp@65$l^NgWsg3x-62@_`f zt2Hl$A;FIe!ZG0sX2Ip8>(aSKDkjBK*k#9{i%^ue%u~Oj8C@Pe$$LK zN?JN8$fRAnSOTM3(@xfW3OoZG3&bD7v~ARI-hM?3ajs1oGKq_=#EVK6)o8_C=qc;K zTq_PWM7C6#HXm~f#@jB=kzpD$zmRrsol6)<*7n6WOV$%JO_)0vsl z#?|Y!@k~?D29V7rVbYl6rN(K~R%t`Qg^mXXK}>__Oq<;9S!`|)ni1S==hI>Yc-%?y zC?5xVO67iJ6A$2}qm}6|q(+hiR7r!-MC@!PlP9~yY!WxTOWL^^Zx~kiz$IZWV}4k< z3Sh+d7hvb;Pf3O`v%~B^5G)*1fj?#+c#N^6!Tnbl`Dv8E~2saz_fb7Le1&W;1XJxNtP6Ai{y6kikH=Vxx z&2nZ+Q<<7~x?zXbc`(m+$va;q^-B9Idcs@Cm*g3@)Dnl9Mh*emV9DP!-}Tw9-4}B7 z?0a@N`u_tD{`K_KbF)*|{>|~zUxl5zwA{ZTe)_Ah)7ySUe!7ad1RS0)2jKExezEU@ z_;v0(?C9#oUv{zIj6H*!{PuR>DkZ+7-4eccxm29RMVDlEM2R^I{;bs^SE~`hKC=C7 z4Mv^KoOJ`wlv?{?tvir?%mjVa+#e4E6u@esx^GPET*r>X_IBf|j*FU7xA#9R*q7LH ziF;R{{~_dPPfngKI}2jKA&l28KJ#5D39iu+0wErfQB6%D8$UYP zk8xfOU}3fWY%{wGNNX<|HQMC^qto||5#Hbs_O%{f=u|!~=15~lvAvqifI7%|S7J$_ zA7+4#LMlC*J@0k<64RpicO{AEVyNP`s?Ur(pgwv9kjRok4c+`+6xOBI`N{czAO5 z2b?VC%t{OtnNhZEjk-9Jfd4L_3!2p%=nJYlbQ)5D03r3IJ<{B(zb_e~m{y{Y&x`_t zw2Q{$P5Lo=uB^(C=d_|`vD`>&pp6Xfid^gHfz9#~%b%7IVBiHnAah)kAGRSaWo9Ri zVQBK53>%Y(zXU#1z&?MMlE%-J!6ALun^bq6JXwP5{pl$Yz)AkHT@)>QO zk8uXbA%jl~V^HGgJAbLQ^PSjs1~|%s0k}k@@EpTE=|DJu>OgB7oqQpi&b>-QWwWpr zFjWUWVe)N}3QY^)yL+=nZ=z#6LIlHB+>@K0gI8Y|CI3@Q@UKo4ice6MdZ;c{jB5!Db=1{m95{NhA<<}}V;&WQ-jIFkkH$vZHZb@UpW zofsEjqh#RIJg`vkyX7)zoHIdA!pO&t1X=Q81cw)zNHK#n_ePXl7-PEe`+Sp010C{? z1l9)&BbqZ3GW~<698NR9n2Xe5RwzD}oMleQd*NUj1O)wwfo=ZUTl+4I&v#>Wv*k+j z6>Yvg0<@1!GX}-17`8f1g@*Mh#P83i(6~s*E=+YCE2lkV-N}VF^f51DLV=jjq?(1q z9LuE1KzDKQt-ixqbb_UXtC1B)^jnQv|scbI4PuAYL zV0R?i0rvsUugUv{pamzV&%A{tW=We5#BONWD#U%>sXOJoC3~j$p8baSes6>tELn7u zy$B8VMudo1LWo9jzjt7AD4NROXPLlzuYP>_f{{F*UE?W(~mEaSIGn3p(S$p z^hYAHx+|Ly;*p5=&$X&p<4fBBtT<#qC){(Z(OY0E=ie3e}59*#uwgA_Z_ zOR*zlH8X4M4g3i!+2gRryj%qqL~$O%k~O2?qffyDx&}XmcX=}uRRfwja~50B;Grc7|IE8f*v<`e}Vs~=OW(szaX1Eb(>rmdmh+VCnMsd zck^xX$~mVZR*&ou)v|63tk>CY$-4FQ3ql)M%R1%#&TVq$!c&oGpt^FbjUR2%VghQg zYtnE9cQkAo2Ol1_#7o}yZ5UEV0?dlR?I z#nxg=e#U$Kdvc~1TPecIkzX9a=J+T ziwlH!-Me6uyb~V}ZIVyn<2gNY9X{UPBj2N>HSy&hxk$^mEpPgeoHe6*)+X>e2xCjsTi&7}+1x$T zl6RU#+BJaard64d# zS$BJN-wJGcZHKMYs_I64qKHl1vq=Y@mA#On%}D$;nxP6M#2o9>Td)=XYe?R#=2_nM z1~u!mcMVGk>c2ZB7h_}o^d7m~`^%JUk*!|j9{G>@(Wm3hmW63ifK?Q`w8rX^rz0{N zUAN5=H2^|@7iKMS8ao=SrPcf5HaSl%w!B|_PtKkA_ifmHYAMh4(urHglkIF4_jraYJ$_L~IZ|Gz4XK329S7vEL{_|;U`+wXkKQ6aS?D)8R(TZ%b zYP{!cJ$XQ zr-mo^|WHS8SpvLF*ydVF&|3P#5DN zz)b(Rjy&?G5^o{WFu^*>s3g0Nf?l#dn!)t^Z;Owt7yb3|zh|A-Z;OCKo=9%Y8AuHx z0wvJUR(0jwVUl!kAEX8l)`?I+OSbvuK`)y_*d_tvHPqVhgdqg2d_ql}%6|i$mpaVF z#e6x{ds6y$x?tOw5}XK##snF#8v$x9sbm^#Xcihi1WfI4Mw(uL>P1*I-D5hWf#z_S zfQ4jxnP!`UrMA%-a8hIWZp6m*#plT$#2R3+p_&y^FpM;Gn}hc3@hJiT9oxx~QIB<| z;n=lfDEcFGrlcb@r<{4tlUXQQ+r2j*m76arKtEICmcTz@McdlMySaCQ9a7>cVBHDGgWmV%bcAmV?g2IRYKrzIBDwNpc{p|DdZn@X%{sMm7@BR4~ zU}i*zYZTOdgOAdM6)~ z3*^I}J}7U3wn#mC_Z*Z9p)LOLLD?Z6@&5K8-tYD*9|w!u?Opr0?1irQdymVT8y_t~ ze2^fc8wgOP_L z2O^I~4n__|4o8ke#B+G1j#}b*Ewb$=m%Rl~%02_v0$CC6t)gO5Zxt2XTSbNU@d=2{Gf&E^RII|=@HM%_TlbV4R5L2P ziKk?ns;%%|e@ZSfKj%dRUf`>}b+zb0)mI4oqbba+5Hn3@)BZv}tY+&!CXW7vOiMMV z!fQPuFNw^lsPR5A0qrhvM7|&T>C;E#>Ko=)Ovc!}3Sg`mUGslbYlXl+8V4^HSM0mL zc8=Hk4cRkoDM7KULM*G;Coj9q`|&sA?XuIm`kQiLq^lzO=_jP^jek=n&x^O~WBFO(erZt z1;?>W>zynjLPlu!cRw$$pMHWrdaj~DJm>xH^YS|NdSfIROpyBU05@lJb4AH|+(G;mk^z z)Es7C>)QGYTX5vw@ASeInHn6QUFq$6LGH$mtUm>8&8+lpIVI=bFuO9$M&?vbVk2_^ z444xAu%gw~VxITFDVeOF&wsbQA)4uz>c$t^L@NQ?;(h!@dD)CN+Qd@4P>U0DZ`Wbt9t`(6salijuo|YfP=KP)0vPtbHfNzlxPWk!ZJFO3lGX2?JMH~B4j(tH1#aua@V>+ifNAC#xO z#9Q*Jn@*Js{L<(#<9w{tvlxdQ}dY9Dl#(5%2DYRfD&%O8p#r|Ej3^w31cc*P?0{ z{ah4N*Lc|r)D88Gn#{w!#;PV@!~IOE8CBjVrmMSDZI$=4>FRgzlNPf%K!+G>$g{1$s)Hhe@p_f(_ z8CkRU)CKAqSk>sL33}3u3ss|P)r}YPk7-LWx@xWx%c{IXpOv#G{^3G3O}U*_Wj=u@ zBKXN4K#DmTm;$lyd}0OdFYvOO?4Mb%V(4IXQdm3mcYUDu1cDWpED~fG;hd%zy{J#t zdh;&atzA{fDuOGRKIy^VLayw0h7*)pknM$$LcS3b^eDQf0Jp4hw0ixHQ4nHv7k+?P zy33o)B0B^zd*ohpDXl1&YE-r)v^Sjnz-y|4DHB|r@Klq8CF4W0=3HG<&(w^N2uG=J z1;UY6B?FuNP8yq~7~oykN$_qsTc<647XiGsoCWjC=}jePM8GZaYj;u<4=rz!i_BDe z+0|~qE-}IF&290t*G)P{mnfX<%A;$)fjqOuLvFYHttT>_m>)(5sNNlg$xQu0`At*V>s zb*@f~r50t!+yU*}?}e|T?v|0vz&0U9Z{*g>ShvU*=Lh*TH zGDTSla8`{MA9gsq==ieNZ&Q5JNH%h!H;JghM50R85vjvJIu~P=#}QM^VsGhSHFQqc zfh;_ZOl};>4DlE+N2;XFUGa-LoQks}VwQ2sVatuCcCq{4v_`gF9mfNXf^TrkHUX2A zJtswI`^FMDJkR&`Z1KKFksE6B@iycK@HjUIc^jHj>j$^ zqUIQ+$(^+zguQoRFOHm%c@;FPp@57caE)Y)JEc9(~=D0-sc9-F#7V)7bQK4#Mwx=hW4 zK`KOu?Np1=bN1qs47zwk%30h+8#>rt71mF&2IWTc9__U)xM%Agdo#2(&#?~C2E9hG z6vLiE@cQNDiw1{Im0Zupq`#bQCQa)J=3?i3ELDmnElU_Ja|QQM+Lew&9&+n84_f{1|oaHH16iscKrKRsF9hzQl#yff?q?Ii$yA={d)(+xdrR>WdM=5|T@-KoWV$ZqZ4%R06X*fll@h4gf>}c_zivCh5UqZ zY&D&}1O;VJ1j(m5y{5WE%GPlLa!4?Cj3#!`x#47i+(&P4R1X;P<5Pd4C0u|L8O>+l zisyajVj>WD*{kV@zgjktZq}6=BMkWI?Lj-Sml(wTRpS10@y%i89QmmYWsv7ZKguu435euLgQHP(qah0yL}eU@7W52K?TUz5q|Y*Z;8K0hdf zZQ??~D{=|=R7>{>z2bdBd*MLoHlcU#yiF#o!i=vighR(kAd%;~ntC#ahdKIkyqowOl@7{U+-+@i~^B0evAK+n{C;{Ld*yI#@k5?P=-!MQ{ z{W)W)ND%+m+2BDJil~tx#zp{tp2QAmrjytDV2P3kj#jSZ?bb@(p>v=%Y<~qr0iF<& z+T}DsuL7DlTuKx4?p@J@o9-~G{}hbk@HqfK3!f+fK+HWSpCFoWQ)!liJ{6R*fb*h> zDR%C`^IBDcmFz!Gbq|P6JoWBp&8n2IEP;oQgmoRn4JQW1gB3ecC6h%h%uVaI=d)-< z5RaT6h@o-HhY_~sz%HSGvLf~WVi_7q*KqapC$GX$68p!i#PRc+YUBF?Lz0AOxtgI_ zcw&?sBLNyE*o#|YR&;`EzRd*b=2<{TP?d@yi8aEhptz0;3WxP_kHLV2T3wtFMHifmbu`${egzu8Gx z_uXk1QJ`ERVuY;7pfPF{mAEWQ8=bAqIow7a8f0?iaM+2z2-ts9;3wqKfn|Oto7K0h zUbk*d@22kG>gv5^OHbFPt?(8ha*|BV$;t(dlLG!vRLRw(ikGemPEt=A6uUv4FVcZR zIBubi{b>VHXPL8Tl+{pLLw*O@$oz!eYSl7YfIn%GS*c!r6aKc&h4W@$+alRQP}2WV zJ`575N&9q06S-}V&2~ccoV`Ai_7N>~vaj5rFiL`5(BcV=DE=`Rc|T1o%KQ6$B( z;Ly58!WD%=61{FA&GZ2H;8_FP9LU3-|3DGKj{b2(oaTDrKI>h$q~*o{bbD;aXfJvG zf+tDD%CM_7$Tz1dZ37t5|3th}4xSstj!_^bd!>KU(Vb>KunBoK$-)R+@77Fdp4mC` zFo^$ZvYq%)T*q;9PL4^hr%VtV$W9FuQ#ay>qvNhuVD=oU>k4Hp4mw9v5!DGIuP=pd z4N@>~Fc8Os=N!23bbvbodiwZJ^7;<>d4mWRUsuSl8w3Lq5$j0`aWh$Nlu2b0h1XJ%Ii>yC6&H1nFxLT9>Ab!B274fnU2TmEQNEIrVOQ^_s*kLJ8t2^TUX2 zARLQ|=$!D_2@aaFcLG+FOQM*t&?x+lAjH`V@-L9OKwWSQ8I#wifX$s0;D^wtVo(`k zI@zDuWsf443)v4eOWJgVLFAYfr{B50+x9aJ;%zJGa0*B_ zv@*?_4??E8Y46z9k3>W72zYd9;uzA|g>XM2O7ca-gVP|8Bqi90>DKT2%R{8cNjE00 zGT89(MR!o$w4O|6bpn;(M78xs7e~VNXHY{1JH|K~5MYVQN_0BmBvF8?7z??nXWhom zTaAc8AY@;U$*pse?;A!6*hUBE0HFK4m=T>R{g-CHhA;&>LjVJg@4@6Sqn*^2i7g-{ zd|@((+@JVWZSXlDgg}VfD|AK>PO^q@?t?B6vZ=Q_4tzM<&*_V|F}~lM$?i-bt0yTg zusWswNV5p+Tm41{1^Q!CelOOEbCR$DkWq;b{j?vGpOx4F4nW*Az6jXF@zZz@2a$>k zt=W>eidLpV(WS*z_t}yza>${GIjSDP*k&y7ri`y#i3GmX0M39Me`Uo*)-#<&f};Zg zZorw9@F$T1*Nxp*QWYqEx4#fhvOA5g{De<&arKj--f}3c27=RXfl};$Vt+31;`V!&C+%t_Q)GqBfm0~|r}PSQmr!W)W%j$M!fv3g1` z#5TZjR5N6}CbKwH7yL>TupWaW?xWsnrjC7%L?iH202VPbKld9Hhk6uvLis@a31v=U z%KRNh)Isv`m7DU6e@^)`uwP=W2@heaf0oUGH53~eGKkV4&P%*OlX<^Ge*?K-!^vh& z{Gvpf_}C_W#0)M^=i^{*T`Uva_cC|7)S4r8>I+g89hMn5@21+OoNDgN48Tv79C@w~ zMW|7V{65_=aewMLS3cBgN@VKtg`Q}@+N>dp*X>Zoi8vlKJP;xY4QxH2j5n?c?k*d^ zNXxmJPj5!P%uK6W(qEV}rzD?MPSH2OK7x8dO284sq)U4nvWas45ZIkA2BjATl8$u7 z7Qd`!o}}#;5CE7Fk)#PoCQd{Z0>M_n+?nch)G$-JKMkGWnL}wTDjbK5Zemu%pE!US z;Bgo=!O5$a<#bc@#sSAcbTblBxl*g4FTY&uRtY{!)m*56Er9D=w~f-=FkQ}Yq|8;W zL`K3FBMk8Pw4B@z<-CF?)0A+?O#%U*L1vOFbPNwsvWod1=U3wm%3T8I#@R^X)295& zv^_SG7;v;O-ss%Jaz$KT1C|}eoeqfR6h|GAPx+lRZBFnu3eP|W*?s_ncI3D{mcbT6 zfcOUN5C}`kW6ra#WN>B}hnDe%ucv9ziOFOj1dyuBDlvpD5_>6m*u~*8dM9R^dem}4XXh`@3XC+lwb|Cg)S-UIg>#QJJ^iT8yb<3ex04&iT|(^ zn;&0w3n{>Cm^qilq!^V<|dCQ zu7(asnhU`Xg&0dZMMt!^^_)Phz^XgUT0|H);1@&0dsusg0*KQe&C-7Yd{#X>)EoM;u1Xvo@D5tV`iS%qr^nnY7WiuLY*pXGXRqPgD%KvtUEIe<9C`#cKZ=e`t(mMh@v2?|4f%uCLYOx z1p*=GPe0zsv>IRHhkj7M^K zpsNAOhlaj~a$b~jL)3|(sigUMyra|AYUO6iuO~y}x@s_Y$Sq&pTuC|L3H=f1D^TVj zh~Ta$W^ikbivNaj1{7HKDGZ%Zw*ib}aF3(RXeJB&Y(0cAXU{nx14yBqxZe6;=sjrK z{~*X3TTXai30eq*P>c06!9f(oF=&BxQWTIYI#;h=+1b+>NXJ#W>_$mHt=6-8KBU%Y z>h|rNbM(|?_R4LjNhfY_t)&5nxCc+bGlNQ{aeL)>Ixz|-61{=e&guv4JcDW`W_m}B z;AtXM#@isr2euWlzR-HIv!sOoSO%nFYI_g5RJt96!uvr>?)W0^fmV-~ZISgvSqB9w z)M0vnEkbOCa^U>tJ-goPIMkd1zQp3{S_#x^WdXaK>khFe2X9egg7r$QzA4#i=XW90 zfw!#xF}4Htd|a4vCBsC!P#K_1Q8QzD_Q8>bQnnBlASZf`S{&f9r8TdJS&-raYK#vf z8g>Qr58wJjVpA6}1zKXUr254`z2%LJoI$Sh8GLR^#%ryU>&t0 z#={V}->qi9q<)ljPj1Pqt*f!pO%!k7FI}BQOC``ev460;42q$XrAuRL&|PM^!wxF2 z7$ZC0SLCuqEsvb8VtLIkiJ4-pxzs(bIiO5tf>^8Ttoa$^8C(H#2{b@rwqWMCH-Qod ziBId+E}8=yy7rE?6hV->jtsZaWjbMZ(tg+81;5iD>Q32gV_tv_W!eybDg9W#Nyb-q z4J303oC(py$KQLW_H}n!%VgJzm76;4u|hxGQZ~+Yuul4;z{kBN12zk_NjtiCW>L+k z_z_AC0q+#KmVi8Pymcq!)yTZ~MXhQ#`J$-pu=*l3Gj^IzRbH<`8bv=&^?5)A`99Q zF?^0zv8Xq8vAR{whs(!0uDf-ai8#ua=%P{M*<% zsxvC^4?s3NS0TEhUf*A#&cGcFYNqOrQe6fInX*4_Pz%%={_`qt-b{6^+7R{D&Qz@n zHk1stdGb)3^-!BlNwp>FwO*zMQPbps%T%k{3d|gqmv}E++kLvT`$C<0e{%)Wd-g=V z3$9eVk-PQDD^=UHJ=FEysMs6+@ZYXf9SiO+8DjtBA@=Jb_7_{u@P5};Uq+hMk~!-7 ziyow|@zKNd^FUM_D1P{t>XG6f)!v?~RDRl{^x46vIB4G3SF5E8$hhlj^?r3I>ixyl z>WT%2N`N{%8K}b=sKepz8|SKlrB9({vEIiK+7L(4oLBb<8u=UIsP2FsDP$xbje5uC zs)yX;CCy(5w|-$#;}azx9}9ncY|_Wim3(|eg}*+6LLdy-<=`*|vgf1G(sDuGf6h~v zPOoWzIm^|7PI~q=suc-t8?RCGn9Ky*d@u`J|a^)im#_YgG$6-E^&5=e`^*%j7cWy$!lY;u^0+rPYQQiXQSD zXe$R<@v^0-z1^5t$REQ0UMZiIUj%8EsS-pVy;g#%({pGMYhz{+Plpz&GYMa-+cPAzPGy=1&I zG~8Q0&727h_m-xaGf}Dqb(weZe0B5m*Mb^EZ+UmmSMd%sh|0N8SMnS@aV9EjIEV$s zpy+h`^rW&H;%%ZqzzwjzJzrfjs{*r9v6#R=uz@#b#0364_YJbc6s{&)f$qPeOUhhxOPT+U4*KbIC3JyVhtWFy3!l1U@@}Im zuheR8hj9lXQe`!T>%NNyu55w*MqwZf60-PBUlfqZTVYL1_US2gY;2+;YkYqmbejxS zA?Z8^M%aC(&38T^rnsy+K2zw5$tL~cChe~)FZd%wSFCzf2|wyB!0D%(o3@^NQeNVA zp92DzrUtW+hKSrE5y4q?7gzHYQ04*8&cT6lMxT zZ8!%|H7j6XLXB+Db*@VDr-aR(W7r9n=s9IjIUB}S*9D|d<E?(2~Ebe8t=3=VE&aV91Cn z2MoXvV1=0FhGsAhnOkPmtcqv%v5Nv)}|RP2n!TO;6l+4xpEZ zfM!VxaZy8Cp-foJT7$~)be>J+zhuKEoRg%7Xkqr84c~Kv4A+*G*>UL5&{_slLsr8Y zD0b<*Yy`Qi3i6nH$ybwwWA`m3+YIgK4~Zv0KX9_R0VlZ^+`_zVoDZd7ZwdL-Od<*t zQUr|mm#p&s60W*Gw8}=L?w=Z8ajKP@>miw{-5DQ#?%;5R-e9F0VPp+BS&IA@vbu2vUD%-?A%PmQ)hykL z;IXC&ETm|IOA-P!@A%&zjL9+ced#N|jTFUGQ@_#Sn9Q3-bfW6w&e=>_zx12Ipnar- zY95`;OCHsjfAoCk%?D87VMqa;yEBDLvr1!_7ty(AFX}aNkZ%yTe6R$dLz4kIqyai~ zz5uP)D06*u>dC`6k#ng)KraPs`J>AYKd{u% zt?mvw({TadvJ~=@3qGjha7HkuCPP{XoCjB0se$UN_6zn2&inN~L3u!kS^qE7O3V#VX^FREu51hb)?a}7 zPn<6(;c*vfQX;AQ|Ykb}Cmt)faViAdq`5rVBL&xI)_+ml6n`hn7SdCcRmm8N&0kXi4DP z_=Gb%PQ@fbbiurslPw=Wd@nK>=q(@S`wRUXE%b?)c%FWqjER$FsC6Ab^|qYkXBlY9 zvY8BWr>YHW?hOwi4X4G3h6IE2!8uhnyyKp2_|W7Un9x93jhi?+i~xhsZ1E3<_-_Ss zl}?o$6d9PMf4c!%R0~B4`4P7Q<>y{1g#aS^Y!H-FZZt&LFURCZ{8E-3bQOD3@RF&R zv@4Ps@DU6SQt|evtRBlclbg9ywUagE0-OBtl-~;Ra;&sUFlO(&FK)^S$G2xH8JDKnzZyQJiQi^yL{xi+N^4_ z&92hgI8gUm*`yLxnACQf)M=U&{YSkX6Zi-Im8ilSnyh*&Ch!lBc1M7Lx6d_op1Gg? zTF!-#tsmwezHU4aa#tB&fzCPOou_?TA$L`S4vY%;bc^Bc2ooweyiZ(Rb_nTKblW_O zVmVE&B5n}y#Z9U!^&EVt zJ)Kng{dkGlNuT&$s5#K zR;9V@;qdk9YAaUdU-ob<8tl6-dZD87err(OcR`D)xjr~(dZlXewGRYm$vIPx*U_S8 zTSp_E(o3|c+r6J}G5kV%uWXt587mdpD}5C8}wE zY$;&fGjU?6`mns{rHE*txb1;$?_=hb+v{z;N&Pt{VRfjlpwQ)$9jfovN37b4VFWZe zndY5N|H5XYb5pP#8N=szg>|5{;Q#e_?^~w+Ir_2XH{X5RpZw?+Z^g~(CMi7kX7y8f z*xU9V^&M2mT)14_k5ks;%hf}uklEj<9tWChZ&7dJe016h^>uk_;yWwUW+~sEnBApR zg_0E$b8b`Ll=95P%eUjC1eKX5Zd{A=ZB$jB_>*<2Q{pV+<@M^HY! zNOn)u-lhH{VjYi2M*e#y*7d4I5nMU&k$&|ba;x{D0reOM7nG5jHQ>oZ7BdUPU{ zMbHEYSXkaC##PhAt9f;q#C0^09qKdksfo|+P}fO$V&X?TRZ1c>;O23)28jRIIEr4L z^nNxDx_)V5)(6xVq&zwC%x|kUi3>*Lz3O5qUzw=?9rXh#U!QozRZ(SCRK~C&cB@qi zKX3Z5x=GEb^mct%oy5e}{w^X)mQ6hPyDC~i`{vJnUu{F--HDn#>KVFy;?y6gE%>qU zqiP^Bzp~kT@T2MgYGvN^F<@tz*SZ%;znzs`-o1O(`{d?{lY7+@5!}-72luO|hGYLe8A%+ecF$_rCin^*!7i@#+1T*&#t;0lx*Z`m zr~eqUI6d({|3sYtw!DJ})U9&s#F+!Avx_L3b)Qo^aH9I{&w-;#-9#Jx=R@(D?$6_EYV*E*k zqp(6`aOvyu=s|pr#6?g#DiGiBwTy^*>!;*SC&^hvclGJZsN2~hC1HvD4nKgEL&P8= zDMIc|L?Od)!Zj+zW5f1#Bw4rK*wWsP`op~|)~)Q~bbACQQ*0SU681w~Vn}ca4*((+ zb=@FOsj~{FPu5S~{iF~@IGBlxMgCEH(B9U_@M zRGAaguV|YQv0PE}jNP>(#Tl$nRP#EfQit8`OwhHGWF&F`Img72IujS&up=iw*U^l_ zPGU#s;y(%{M5YE&j7cmi=3wUXE*~!<#vRp5J0-^l0t*N!&m|C+Mq%q5aEib~%39*o ztKhB$8V;2{@rIHDMiL)L0gY(dhx43Nas-*CqO=_83W`{FvQliRvRU51`z@llS_#2( z354Jwl5#YIP(T>u10ZJp*jzsbf+o~#;}}O=fkI)+KDw|YvUJNf(Hlkh5sE&?0i6^O zX+Gu`_$X)uK#`K}7M!@~&|w{k5kpQyo3O{gn_sZkP&jd}p~I+}}zL zYk+UUbZKtE@P^8$8%l&D@JIvDA))N*K(?OGI4HIbm)F{K{~;<#Al7vp<$+75fRIrF zyFBS&ikt(wix>H6K}G`+1vV5eR2GYH3c!)ntiDK;l&zg%*4_U>dQr!(`=9yEb7vbS zXVPo@fhi1{iAIN+GES1-^1Ia~F0OSO&LFOImVLF2NMlFLr!D|J=14KE6kVDn_z@lt z6$;fQXkQQ=0`Y?diqtHkne$HQ#VEp?k=_Mxh(qCu5+cUXO+CR1x`?c%24#=}PI^j7 zkR$L=jZl!Uy@{`tr{{V)3?$TRgw`UJ2q1C%a{`wur;|+q;d8BAYzX!`a?G*5#s?iD zU2rQR?{QBsksEUIDEgWD-vN6GyNL3Mig8Wmqb8!RB2{Tnn3lv`Uf@;eOJeJ^b$p0~ ztv$c`Q?z}wutKl8g-Lv)5%u74vE7^VDb?U&>yj3wqm&8T6kAT&CIoMB_Pj%w5bZt) zsuj2QuIk*h3X*5@+8%5@ziJDK8DiZMRd}$I-_R1lV54|-Qm`Nc2q>pP@PLZ$^7;W0 zgUEVKf#Vjb!(<4OJAjfeOBOl-v8ua)p{~QO4eTL&#s$b@?Yf>WebF=*6~J7**^AUY zH?IUiFgAm#IuezAYB>goH;u2W`#UI5-XFLqa}(ixR`kEj{{qXSY=_?4ZT*EZw$4!2sSKENC{odjjWYZ!Wl4n|4p zk*^md6p*w_as6PV$P%NQj}Wn~L-G@p{LD8nYXSi^m6V`aHz>u+ReX(_1$NBu!2gFR zbBsdcO}sp1M1X&QSL0+7e8Tj}pLfy816`4>-x872z0YwW??ph|s3jUvA%i`zsGvel ziet-{-*REF*z`+Xt!8#bIc}VN5Fg~l;N%4sQIzsdQjrRquo^}?8(Y|=n4%EtTP*3K zS%8vD3^G^Ksop>hp7H7s786KdM`k^52ukL_P*`J%LDFl7_;wqY4JYCvG=*+x7RQ#0 zS9h$sv^WV@)K&aSb0GEu&{<#kHQ;w4{LeXoyNheUdKRV0n3;U{4{s_?M2NYd15k0O zu*G>S269(JYF+81mQsHi>j#@Ms6K`&u6mXAYGK)skP3<% zanN7M0O%c3pB8)PFw%xY3InnqvBW4~fJA)Skv8pDfBw;w z*_|oCOYi-WpmTEi3Pz!F8!ty5>bLlav_Q^of1!>Dkn&% z!F)=m!$X-Bh>PW&lve{gjF<_%dd8A2Y#4rAVxJLT#dLMH76u?59!}`O3a%hw*qv;& z_g{YDOxgxDQe*Y5uI}~CD0sQGaX6nJ%e7yBJ%ng3?(_V3COdRJKG{T%n_Jz*i{~3Y z(!DY+m=}Olm}(2@6!dc3zl}-JEq*A~fGa(zk}bifITb8d}rzj z>OP^D^*BQ>RBH>2FrT545ridL;I5uk`bk{$ph^IS>hdr2(~i5&iseH5P4`XP@2z%y z4$=bShakXB!@S1alV2hrD4Bs>iVc*v`+!a=PR0S}R2qlEWl$+WvtOet%6(7`435?%wF~LeG$nf&sb1g;Fc!GN7oPMr>IpP$g(_MS zk2;?(ndXmK%Nu1d+2{e)+I_km1hz@@;tWdo)iFN$9fZ-q8g(N5%i?5sL$KiCR5pNu ziIIJQ7KCT^QH08IX${no<=FA@1pS+KAQWrbP@MI@QwJ^6Qy99zZIN7Ugbshq)me|HeC zO$Aprz&XQK3j&n~oOT%c85ReS&eVVe7no}767^kl&sMnQtxoXHW@MSc^_$ElHKm=YjQv`=mO$xj%(zvdXiggeI zNOht86;$5~m>s*7;=;Z)k=q0n#A&N}HG=pc=(%iC1?5*XF9iL;DyJ<(r!LHf$p9rJ zb66!$i%|{_7)&S!dw4Tl(E%WNLaelfzHfv)#~zta9Hb8^#H9PTx2Lf|xU@Mge$J<; z##y#;ejn0_M-X$VOP=+QgQok+*#oG$&z}i$_%(h&ej3`MQq;}_iJe;jt0+PbAV8>J z@_`M#@c|+I4}!cnNF2$(b|(pKGVrNZ9luZ~gTig~4mPJB;7d9{@NWkVsm#7^< zmJWTYUA}SgfW5IO3&1dLH9ML13n+jFfo{+uo*D%Yn=QcKZ&o7o258fZCS-xV}8Q8W7eE1H1Vz@uj z*2d3zc9|y+M%r5V2}PXeQn`mC9r}wksO|s@Fh71E($UCYARB+cc{Ebf#D8fCbkIvb zrY_%bC{nYMpZa$#lgfBFa$lqc(i&w(wHxqAqy`m@*v`BLJPW3Fz zlnyq53$o%q)$ z)Rj^#je2!oRS&4HsQ36+)rWA{cKw8^kg=myHC+>hoYZAcB2(D>iJa8lC)IBwqjciM zC)JlEj)?YtP5o`9a-tLW996GNm5NSm{g&#DsL|-eH@>aAgO zyQAKJe?~3A{7yZiKB4Z9diR{b>FI+}>m%MTP9Qn!*a;-OJ{a{*RtC;^_}UD zM0I>y+oMtM?eD55sk@2qsjpd2MeCmMj(%HR?0xWuYPz@YS#^gx9`%0uEY8DDM7`{D z>f1OXUHW}>AcEtqzkOcK0)pRs9yycsv5AF$slKVy?3nk)kJKMtFfT^A=&YK(F+b)# zbW%N5{_KvwR{yD5W8NJ%SlEf){7;h z1*5QAEU>urDS{qJyUw7`Qm@9l!XX`SarsUG-`_Wths!v)+>nRM_fQrcU4fMbiQ7ef z1L%fuo^U<0+*pQ;eWQuNH!0q zNBOcNE`HzMglZ|l(cgMBaq{i${fR8Dg2NwlpL^N;LtaJ zUj!LoVmq(;Ro>m&Eas$aEr zo`JzS^nC)DOr0WfNr)HSO#0A@3hC&jnVM)q&@k-`p-4J;R$M5j(c-?P!spQkV|8D* zHx$S#>r{9RrS50HTyRII!)A zWp?X<*pXNRxu)n7tr0kWIXzx2&PmOQ>h(a7)KUp!;Hb!75;3af=}eM;i2Q=8D>}DO3g@` zL4hf<<++T#a|<7~+%mqFFQ>%$Gy5??Ot9AV1t^_Bz zD1iuq)`fU?z`75_=1i-8+D6J5+gio7W(|iMl?r?{|7B| BBSZiI delta 8155 zcmaJ`0a#R3_P^)c_vQ_P3g#pz7#Jv!D3s_>V4||g7AXoTn}j1gb&?ri7*sMdDz~+k zHrJ(-eNyr5vMpO=*19j++_JQ^f0kslS?dNlqWZLx(IG%YZ;`S@Y499vVb^e!qkHg`#_1ad_b zjICYcsyB-?7`sT_2pDUwGrj&U30i@GSbt5eyRyDaLhKD@Wp2rAA-be}xOUEKIy0o{ z0)-*Kc|p~CCDg9+xjbe?y;45?(hHNd6B3@M4?l!U=zT9t#8Z;ac_ByBHTWHE`#EcO zaSe=z2=dfGHaax=t`#TJOR6u9j@R%66KGT|SNYhC`zHS?-n_4NW0Yh8`?6{}41kc8+(d@sa-pt@%@@OX#R{d9(Tv;m5so4#y- zV(6hc>mUJ+(M{_hMIM(xt_?7j9$E(lV|pbdm-yUffzMat6QhLaroMHsjCp>%9=e!E z+W-sUIF)RGtFc#FEgN74;0d{XUtAImdX|U^oX(Kf)cClICQ0=#~-A8K$a9D!fzHqH3q6Po){`6_$Fz*o%B8I;I z1?!Ft?y1v|Z2kCtn4w{urlg9qcKiz711&*|s-l=$w!`}$fNgAmlZcli#ac=r0Wz!` zAA~n9h7?NQ1uG$qns>p(xHJvs6ng9ZzJ^La&pf!$C`20V-vwomK@*>X2Qf>dwgXx7tkALLlCJPm)<4FplO-0>irT zS$JKCd@J?O@ass(r`Hd`A}r7zNQa45?rZQeK;a)>hiedusH+=F#}sSq8%j07m(c7^ zrzoSux1elBxd!v+)vV8*Qtx;9-IY@lS$OsU<(d}7-5P7sr3p>&j|lweC`{^n3zmbM zMjYkFL#vO%Wc0A^8rWB{BXT(FH1;~N-Ws3BRb8inRw7_(O|9wU5o){_x{^IEZzlWdY(E^4iNG_XfsU*k2IIeJNr$J9g*lk$e?(a8HTukYFqUNV7kMotUD?lPvb-TakT6@j1Ad zHV;6uBSTlVqls*N@D$FV?$6o#eDx2wc2t%Q@j=JRf@i50v9#iIm=uw%2F=h#P7v&V z9Va;QLSXq}V0ntiACa%f^K?-VMqU^qFA5_sr2JEm727Z4=enw0-bxep3p?1|RsHN! z3#%ye3vfgfDX>Ca6o-M8hQP|hz)FK)W%>`n%Jd6hjxzl}#2ob0DM%bquEr_Vg-a(s z>Bd`UPVsRkO+6Gz758ym<<>*4a%&%4G}@z<%%dyM!%eJmqA7Hw5Av~251b`a{>kTV z!=T(Wb0UsmmlIf#fD$)_Cejo(kxjJgG+as@0*+UiI!ZLYG2g)3P zaazxX*I#}MDcZ3MkNkFW%yA`bw=Q}EV(u)IwD25TGjD^L+Q|!~ZwrfSRU)y8K9iuZnuEv#Sg}27fau>ITf39AgVE zh{HCxbz=h5XPm;pH^@(Ml!~}3Dj~q^)cR2*(Z`4#?A)3q=5V91lG8$18DUsy0j%i# zB12)N+2~EcqNx36M2>pSwuwAy&@kiTd|Ome{xT@A@kUK_Rl7OT_th8Dvl^z4%xAn3 zcja2sFGN0lrQve=2sy4;p<^7!&1!7=M&iY^Pe*S=u>vl#iBijH!)qg;jP4qZzk@O> zBO1p^K57dtV!gWP)r+t&&1DN$sA(IfP?OPEp(f>w#nq8+e$wo^bP5kWJ{Gq_9bN6f zsoVsFXm#*5Y_OhkV28v;+hD*$8{_bGKKd7o$GNyE{KXUF@mmfCIws(3T<@Wi6EGgP zga{JjF@e*y+3{G744`1`~*evG%L1 zx6#H-+@RY2Oq>k80kBcwU?*&WSoQWSd;{>LjTT*jH{vN9bzXtD!Kp!Oi0lhUl-h3- z{e!)D)4-2wyVV%ntr2v~ zOcrt^wasKpjG;GXVk(Xdvc&|N5e|c=9$|<$gWz>Yu-a$g2;?LF`kV2}P{7C;!s9n% zTtt#0O)*3Y9ln_*N~3c(*lx*SZeF!)1>zJrOg@;?aE4O= zdCU_?YDRJpbv~=f3v%Td^yXT;2J;Pyt;VGI0s|J5de?eu)_Y5QE^oankd-KWreMk> zT33x9VL|9&qX!pap+T>EFd2&s`lkoeRGZ+%adE{wgit?NY6!mAsM;Y`lo?dy#p1Dr z24og`*O)#o3IvN6X|lo3tiyC{3Or7tyXtTXZZW8{4ztp? z3>jtX@KLq~N7));PWNFIHXC%m4_ASM{^i3g+-6YmhaB3?smBfJ+lBzP3m4Z#;{RqHUso*_70!*RNTFkJzd6v|nTm*ZZ8JnQ-N*g;>dN9TyW z3U!|$_62BM=?8{j9vY5$D2RC|gxR|RpJDIV(umW?99CHTzFW0DhUf{=oNL5a@L1^K zvs=(V;<%#dHAHWy^WKeY96a0SHsVcq!l3cD;>7e5Lqs__T$GbRQBHgAotw`NVShKA``uy814D@ShZFA~Mhy0F z!S-S}^#x{Q>ACXCAh$^5go^r_xT2A}85c2A^JcuB-S1yFnsAOS_j*$1bW$M?Jk;_L)>;&}Sxb{r2~1Wo8*?>w#vr^Tn)AvKW4t9s>Z z0m;=hm9AU*Yzys!6g#bRoB@?5YZXT$Q>|XbcvJL>dEPmlQ0}3wPk;_$>9DZ5Mp; zUOSUx7kqKH0U|Fj&&65!LqshYqMib#J#S|r^%R8E^YEP*H;xC2f;r{}v$8?u{T04I z(JY1DxDzMvarKWoapHNEOfMWVjyive&TAy>_$lHMc@c{2RDBngV6mNEx(k<2FCD^s zkeSQQGanoju`I}3W~ZVpSeI2kgsIrZT(0w6Dlk1iGgpw!Wmhq43XT0KW^6Wx@TvhX zu!ap7;&lH{3qN1v4wPyk^Fon(n>M7#&DI=P!(2sASGRHvF-1RL@!rU_Yz~tjY7NA< zueM?~$D0GK9PW40tcNiZPw6ze7ADf&4>MsY{q|v;&PnMz4`VKJko9#f#8c)Yn20BA zG_8(n<<*aHI2%XzJi_sbSVX<*vCaDa5ljJ26;s-<3}Lsm`PX6bKUNSCxc=<2gYzE2{0)LB0>%^}gcoJtpIys-j&tV%S@4{C(6#rruZd3KP zr*J2K{g|*DpJbi9wi~};Xa4fj_&n^fZhr<#IiR%O`2(Vk$2IG{E_?x?+j{6vY-3z6 zvcBHSM=qY#tYt4?J{M*6+bQNzOr+;u#DkGs$q~_okXW+Dq$4k4ob|hXcoVRDF2!c@WqQxNTH=JF{@XpQc+>m&ge%+Q_E|ps=u$j zhUcVg=ej;TigPUcn>a)ArpbE?&p{659Oaa*fIdEo)1a8fyp2;;d4A5@c##(C(na*} zIyu+s*S?GWoMX9;;d1s_e?EpOaV85dL~h9j{_5aiw>8gaR=GKMbJMrS@K>N)+up-} zNn3;p0P5(zzu{xNa5F!^8==wq5w!Rmw(+5G<{WD$j{fm27Rm%$ z63raIrI2OaH-LYTkY~*U*#*3A11QJo(IA>;TUSZR^C+cuP5vK$FskEZQ?vGOQVp62?`y;1TCXtG|2k~ZLC@+%`{rR=bE($rCMAy+wX z9VM^j=<(nv*$;=TKaZA&)Muoh#mH+|lp`^+%*Mr#^jNu$6QQ8&z-n`HEyK<|=Tv+@Qm z1hAuTl1^u!uVh20XNz=Z270bBy=J|;eyhw4QWVwr{9HotH#W=MR7J5|!B?6sGJ0x& zA?P%A$mkXG*0_AD&9L+$WU{R?deM^L*V-WQVzaW^<@Q8JFN&U5<1?e;*9FBAO*G9Z z^VmYFozlsBsog36$obOp99f`c8PUI?iX3?{{V7MjhS>()eWiR|Yt?cLT6&dyQx+N> zl$I;kMkW~1{4v2*Xq0hQn=8L^lpA*chE^?grE+o={NZy=uFJ4~ohJ_>rxccFj=TiW zW6*v1@2+>mt+q~W<}$ej2MqdR znS2GndZtXa$%(o>nggQh#Os2AfQpRjD?*FobdaxH=3u_v>MWNpAQsxmyFxxRs@R^W q;wTkBoGG=_#V+~85BrX| = { @@ -130,7 +131,7 @@ export async function createBucketViaApi( ): Promise { const api = getApi(); const providerUrl = opts.providerUrl ?? DEFAULT_PROVIDER_URL; - const providerAccount = opts.providerAccount ?? signer.address; + const providerAccount = opts.providerAccount ?? DEFAULT_PROVIDER_ACCOUNT; const signed = await negotiateTerms(providerUrl, { owner: signer.address, From bfef55c97d60db231e5b6677c76cc676354e4ab4 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:55:10 +0700 Subject: [PATCH 31/44] chore: resolve user interface build --- user-interfaces/console-ui/src/lib/storage.ts | 34 +------------------ .../provider/src/components/Header.tsx | 1 - 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/user-interfaces/console-ui/src/lib/storage.ts b/user-interfaces/console-ui/src/lib/storage.ts index 1767ac33..0c749323 100644 --- a/user-interfaces/console-ui/src/lib/storage.ts +++ b/user-interfaces/console-ui/src/lib/storage.ts @@ -8,7 +8,7 @@ import { getWsProvider } from "polkadot-api/ws"; import { getPolkadotSigner } from "polkadot-api/signer"; import { parachain } from "@polkadot-api/descriptors"; import { Binary, Enum } from "polkadot-api"; -import { parseMultiaddrToUrl, resolveProviderEndpoint } from "@web3-storage/papi"; +import { resolveProviderEndpoint } from "@web3-storage/papi"; import { EncryptionKey } from "./encryption"; import { type Keypair, seedToKeypair, toHex, toSs58 } from "./crypto"; @@ -417,38 +417,6 @@ export class StorageClient { // --- Provider Resolution --- - /** - * Resolve the HTTP endpoint for a bucket's primary provider by reading - * on-chain bucket data and provider multiaddr. - */ - private async resolveProviderEndpoint(bucketId: bigint): Promise { - if (!this.api) throw new Error("Not connected"); - - const bucket = await this.api.query.StorageProvider.Buckets.getValue(bucketId); - if (!bucket) throw new Error(`Bucket ${bucketId} not found on chain`); - - const providers: string[] = bucket.primary_providers ?? []; - if (providers.length === 0) { - throw new Error(`Bucket ${bucketId} has no primary providers`); - } - - // Try each provider until we find one with a valid multiaddr - for (const providerAccount of providers) { - const provider = await this.api.query.StorageProvider.Providers.getValue(providerAccount); - if (!provider) continue; - - // multiaddr is a BoundedVec — decode to string - const multiaddrStr = new TextDecoder().decode(provider.multiaddr); - - console.log(`[StorageClient] Provider ${providerAccount} multiaddr raw:`, provider.multiaddr, `decoded: "${multiaddrStr}"`); - const url = parseMultiaddrToHttp(multiaddrStr); - console.log(`[StorageClient] Parsed URL:`, url); - if (url) return url; - } - - throw new Error(`Could not resolve HTTP endpoint for bucket ${bucketId} providers`); - } - /** * Get the provider HTTP URL for a bucket, with caching. * Retries a few times if the bucket has no providers yet (agreement pending acceptance). diff --git a/user-interfaces/provider/src/components/Header.tsx b/user-interfaces/provider/src/components/Header.tsx index 6d49f062..323c5d57 100644 --- a/user-interfaces/provider/src/components/Header.tsx +++ b/user-interfaces/provider/src/components/Header.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { Link, useLocation } from 'react-router-dom' import { Server, - FileText, Database, Shield, Coins, From 75b8f52e3b3de67a256e3c06fe45afcffeda2242 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:05:46 +0700 Subject: [PATCH 32/44] fix: widen replay window to 1024 bits Bumping REPLAY_WINDOW_BITS to 1024 grows the bitmap past the 32-element derive limit, so Default and serde get manual impls. Also fixes a hardcoded 32-byte bound in shift_left_le left over from the 256-bit window, derives BIT_MAP_WINDOW_SIZE from REPLAY_WINDOW_BITS, and updates tests whose anchors/expectations assumed the old width. --- pallet/src/tests.rs | 13 +-- primitives/src/provider_replay_state.rs | 100 +++++++++++++++++------- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index 9c7c1c96..df85e733 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -1199,14 +1199,15 @@ mod establish_storage_agreement_tests { #[test] fn accepts_nonce_at_window_edge_and_rejects_one_past() { - // After advancing hsn to 300, nonce 45 (distance 255) is still in - // the window, but nonce 44 (distance 256) is one slot past it. + // After advancing hsn to 3000, the nonce at distance + // REPLAY_WINDOW_BITS - 1 is still in the window, but the one at + // distance REPLAY_WINDOW_BITS is one slot past it. new_test_ext().execute_with(|| { System::set_block_number(1); let settings = default_test_settings(0, None); let provider_pk = register_signing_provider(2, "//Provider", 5_000, settings); - let advance = primary_terms(1, 1, 100, 0, 1_000, 300); + let advance = primary_terms(1, 1, 100, 0, 1_000, 3000); let sig = sign_terms(&provider_pk, &advance); assert_ok!(StorageProvider::establish_storage_agreement( RuntimeOrigin::signed(1), @@ -1214,10 +1215,10 @@ mod establish_storage_agreement_tests { advance, sig, )); - assert_eq!(ProviderReplayStates::::get(2).hsn, 300); + assert_eq!(ProviderReplayStates::::get(2).hsn, 3000); // Distance == REPLAY_WINDOW_BITS - 1 ⇒ accepted. - let edge_nonce = 300 - (REPLAY_WINDOW_BITS as u64 - 1); + let edge_nonce = 3000 - (REPLAY_WINDOW_BITS as u64 - 1); let at_edge = primary_terms(1, 1, 100, 0, 1_000, edge_nonce); let sig = sign_terms(&provider_pk, &at_edge); assert_ok!(StorageProvider::establish_storage_agreement( @@ -1228,7 +1229,7 @@ mod establish_storage_agreement_tests { )); // Distance == REPLAY_WINDOW_BITS ⇒ rejected. - let past_edge_nonce = 300 - REPLAY_WINDOW_BITS as u64; + let past_edge_nonce = 3000 - REPLAY_WINDOW_BITS as u64; let past_edge = primary_terms(1, 1, 100, 0, 1_000, past_edge_nonce); let sig = sign_terms(&provider_pk, &past_edge); assert_noop!( diff --git a/primitives/src/provider_replay_state.rs b/primitives/src/provider_replay_state.rs index 81c26dc8..f165f567 100644 --- a/primitives/src/provider_replay_state.rs +++ b/primitives/src/provider_replay_state.rs @@ -18,29 +18,59 @@ use core::fmt::Debug; use scale_info::TypeInfo; /// Width of the sliding replay window, in bits / nonce slots. -pub const REPLAY_WINDOW_BITS: u32 = 256; +pub const REPLAY_WINDOW_BITS: u32 = 1024; + +/// Size of the acceptance bitmap in bytes. +const BIT_MAP_WINDOW_SIZE: usize = (REPLAY_WINDOW_BITS / 8) as usize; /// Sliding replay window over the most recent [`REPLAY_WINDOW_BITS`] nonces /// accepted from a provider. #[derive( - Clone, - PartialEq, - Eq, - Encode, - Decode, - DecodeWithMemTracking, - TypeInfo, - MaxEncodedLen, - Debug, - Default, + Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, )] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ReplayWindow { /// Highest sequence nonce ever accepted for this provider (window anchor). pub hsn: u64, - /// 256-bit acceptance bitmap; bit `i` (counting from the LSB of + /// [`REPLAY_WINDOW_BITS`]-bit acceptance bitmap; bit `i` (counting from the LSB of /// `bitmap[0]`) is set iff nonce `hsn - i` has been accepted. - pub bitmap: [u8; 32], + #[cfg_attr(feature = "serde", serde(with = "bitmap_serde"))] + pub bitmap: [u8; BIT_MAP_WINDOW_SIZE], +} + +// `Default`, `Serialize` and `Deserialize` are only derivable for arrays of up +// to 32 elements, so the bitmap needs manual impls. +impl Default for ReplayWindow { + fn default() -> Self { + Self { + hsn: 0, + bitmap: [0u8; BIT_MAP_WINDOW_SIZE], + } + } +} + +#[cfg(feature = "serde")] +mod bitmap_serde { + use super::BIT_MAP_WINDOW_SIZE; + use alloc::vec::Vec; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + bitmap: &[u8; BIT_MAP_WINDOW_SIZE], + serializer: S, + ) -> Result { + serializer.serialize_bytes(bitmap) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result<[u8; BIT_MAP_WINDOW_SIZE], D::Error> { + let bytes = Vec::::deserialize(deserializer)?; + bytes + .as_slice() + .try_into() + .map_err(|_| serde::de::Error::custom("replay window bitmap has wrong length")) + } } /// Reasons [`ReplayWindow::try_accept`] can reject a nonce. @@ -88,25 +118,25 @@ impl ReplayWindow { } } -/// Shifts a 256-bit little-endian bitmap left by `shift` bits. +/// Shifts a `REPLAY_WINDOW_BITS`-bit little-endian bitmap left by `shift` bits. /// /// Bit `i` (counting from the LSB of `bytes[0]`) moves to position /// `i + shift`; positions `< shift` are cleared. Shifts of /// [`REPLAY_WINDOW_BITS`] or more clear the entire bitmap. -fn shift_left_le(bytes: &mut [u8; 32], shift: u64) { +fn shift_left_le(bytes: &mut [u8; BIT_MAP_WINDOW_SIZE], shift: u64) { if shift == 0 { return; } if shift >= REPLAY_WINDOW_BITS as u64 { - *bytes = [0u8; 32]; + *bytes = [0u8; BIT_MAP_WINDOW_SIZE]; return; } let byte_shift = (shift / 8) as usize; let bit_shift = (shift % 8) as u32; - let mut out = [0u8; 32]; + let mut out = [0u8; BIT_MAP_WINDOW_SIZE]; if bit_shift == 0 { - out[byte_shift..32].copy_from_slice(&bytes[..(32 - byte_shift)]); + out[byte_shift..].copy_from_slice(&bytes[..(BIT_MAP_WINDOW_SIZE - byte_shift)]); } else { for (i, slot) in out.iter_mut().enumerate().skip(byte_shift) { let src = i - byte_shift; @@ -156,18 +186,23 @@ mod tests { #[test] fn nonce_at_window_edge_accepted() { let mut w = ReplayWindow::default(); - w.try_accept(300).unwrap(); - // Distance 255 is still inside the window. - assert!(w.try_accept(300 - 255).is_ok()); + w.try_accept(3000).unwrap(); + // Distance REPLAY_WINDOW_BITS - 1 is the oldest slot still inside the window. + assert!(w + .try_accept(3000 - u64::from(REPLAY_WINDOW_BITS) + 1) + .is_ok()); } #[test] fn nonce_past_window_edge_rejected() { let mut w = ReplayWindow::default(); - w.try_accept(300).unwrap(); - // Distance 256 is just past the window. - assert_eq!(w.try_accept(300 - 256), Err(ReplayError::TooOld)); - // Distance much greater than 256. + w.try_accept(3000).unwrap(); + // Distance REPLAY_WINDOW_BITS is the first slot outside the window. + assert_eq!( + w.try_accept(3000 - u64::from(REPLAY_WINDOW_BITS)), + Err(ReplayError::TooOld) + ); + // Distance much greater than window size. assert_eq!(w.try_accept(1), Err(ReplayError::TooOld)); } @@ -176,14 +211,25 @@ mod tests { let mut w = ReplayWindow::default(); w.try_accept(5).unwrap(); w.try_accept(7).unwrap(); - w.try_accept(1000).unwrap(); - assert_eq!(w.hsn, 1000); + w.try_accept(3000).unwrap(); + assert_eq!(w.hsn, 3000); assert_eq!(w.bitmap[0], 1); for b in &w.bitmap[1..] { assert_eq!(*b, 0); } } + #[test] + fn duplicate_detected_across_large_shift() { + let mut w = ReplayWindow::default(); + w.try_accept(100).unwrap(); + // Advance most of the window in one jump; nonce 100 lands deep in the bitmap. + w.try_accept(1100).unwrap(); + assert_eq!(w.try_accept(100), Err(ReplayError::AlreadyUsed)); + // Distance 1001 is still inside the window and unseen. + assert!(w.try_accept(99).is_ok()); + } + #[test] fn bitmap_shifts_track_distances() { let mut w = ReplayWindow::default(); From 5f8ef54113c5858217b4fd2fd22a4c62d565696e Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:44:57 +0700 Subject: [PATCH 33/44] feat: bind provider-signed agreement terms to a bucket id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `bucket_id: Option` to `AgreementTerms` so the provider's signed quote is bound to the bucket it targets: - None for primary terms — the bucket is created at redemption; `establish_storage_agreement_internal` rejects bucket-bound terms. - Some(id) for replica terms — `establish_replica_agreement_internal` requires it to match the extrinsic's `bucket_id` (TermsBucketMismatch). Mirror the field as `hasBucketId`/`bucketId` in the precompile Solidity interfaces and decode it in `decode_terms`; guard the example contracts' primary entry points; map it through the PAPI demos' negotiate helpers; update the provider node's /negotiate, the client SDK wire types, and the UIs' terms handling accordingly. Also use `CheckMetadataHash::new(false)` in the paseo runtime tests to match the runtime's eth path — `new(true)` requires the metadata-hash build env, which tests don't set. --- client/examples/complete_workflow.rs | 1 + client/src/admin.rs | 1 + client/src/agreement.rs | 7 +- client/src/lib.rs | 1 + client/tests/common/mod.rs | 1 + examples/contracts/IDriveRegistry.sol | 6 ++ examples/contracts/IS3Registry.sol | 6 ++ examples/contracts/IWeb3Storage.sol | 18 +++-- examples/contracts/SharedTeamDrive.sol | 3 + examples/contracts/StorageMarketplace.sol | 4 +- examples/contracts/TokenGatedDrive.sol | 4 +- examples/papi/api.js | 1 + examples/papi/sc-api.js | 4 + pallet/src/benchmarking.rs | 14 +++- pallet/src/lib.rs | 15 ++++ pallet/src/tests.rs | 73 +++++++++++++++--- .../src/IDriveRegistry.sol | 6 ++ .../drive-registry-precompile/src/lib.rs | 1 + .../src/IS3Registry.sol | 6 ++ precompiles/s3-registry-precompile/src/lib.rs | 1 + .../src/IWeb3Storage.sol | 6 ++ .../storage-provider-precompile/src/lib.rs | 1 + primitives/src/agreement_term.rs | 5 ++ provider-node/src/api.rs | 1 + runtime/src/lib.rs | 6 +- runtime/src/revive.rs | 2 +- runtimes/web3-storage-paseo/src/lib.rs | 6 +- runtimes/web3-storage-paseo/src/revive.rs | 2 +- runtimes/web3-storage-paseo/tests/tests.rs | 3 +- .../client/examples/basic_usage.rs | 1 + .../client/examples/ci_integration_test.rs | 1 + .../pallet-registry/src/bechmarking.rs | 1 + .../file-system/pallet-registry/src/tests.rs | 1 + .../s3/client/examples/basic_usage.rs | 1 + .../s3/client/examples/ci_integration_test.rs | 1 + .../s3/pallet-s3-registry/src/benchmarking.rs | 1 + .../s3/pallet-s3-registry/src/tests.rs | 1 + .../console-ui/src/components/S3Tab.tsx | 1 + user-interfaces/console-ui/src/lib/storage.ts | 3 + .../src/components/NewDriveDialog.tsx | 1 + .../drive-ui/src/lib/drive-client.ts | 3 + .../papi/.papi/descriptors/package.json | 2 +- .../papi/.papi/metadata/parachain.scale | Bin 225410 -> 225746 bytes .../shared/papi/.papi/polkadot-api.json | 4 +- 44 files changed, 187 insertions(+), 40 deletions(-) diff --git a/client/examples/complete_workflow.rs b/client/examples/complete_workflow.rs index dd58369f..38e30030 100644 --- a/client/examples/complete_workflow.rs +++ b/client/examples/complete_workflow.rs @@ -73,6 +73,7 @@ async fn main() -> Result<(), Box> { duration: 100, price_per_byte: 1, replica_params: None, + bucket_id: None, }, ) .await?; diff --git a/client/src/admin.rs b/client/src/admin.rs index fa4bdab3..29b34d33 100644 --- a/client/src/admin.rs +++ b/client/src/admin.rs @@ -77,6 +77,7 @@ impl AdminClient { /// duration: 100, /// price_per_byte: 1_000_000, /// replica_params: None, + /// bucket_id: None, /// }, /// ).await?; /// let bucket_id = client.establish_storage_agreement( diff --git a/client/src/agreement.rs b/client/src/agreement.rs index ebe45204..381d43d3 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -18,7 +18,7 @@ use codec::Encode; use serde::{Deserialize, Deserializer, Serialize}; use sp_core::hashing::blake2_256; use sp_runtime::{AccountId32, MultiSignature}; -use storage_primitives::AgreementTerms; +use storage_primitives::{AgreementTerms, BucketId}; /// Concrete [`AgreementTerms`] type for the storage parachain. /// @@ -48,6 +48,11 @@ pub struct NegotiateRequest { /// FIX: Safely handles the JS BigInt sent as a string #[serde(deserialize_with = "deserialize_number_from_string_or_number")] pub price_per_byte: u128, + /// Bucket the quote is bound to. + /// - `None` for primary terms; + /// - `Some(id)` for replica terms — must match the bucket targeted by + /// the extrinsic. + pub bucket_id: Option, /// `Some(_)` to negotiate a replica agreement (per-sync funding + /// minimum sync interval); `None` for a primary agreement. pub replica_params: Option, diff --git a/client/src/lib.rs b/client/src/lib.rs index 8f822918..faea386a 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -64,6 +64,7 @@ //! duration: 100_000, //! price_per_byte: 1_000_000, //! replica_params: None, +//! bucket_id: None, //! }, //! ).await?; //! diff --git a/client/tests/common/mod.rs b/client/tests/common/mod.rs index 5c4ac417..f48c5c5f 100644 --- a/client/tests/common/mod.rs +++ b/client/tests/common/mod.rs @@ -170,6 +170,7 @@ pub async fn chain_setup() -> Option { valid_until: u32::MAX, nonce, replica_params: None, + bucket_id: None, }; let sig = sign_terms(&alice_keypair, &terms); let bucket_id = admin diff --git a/examples/contracts/IDriveRegistry.sol b/examples/contracts/IDriveRegistry.sol index 576583bd..3e34df15 100644 --- a/examples/contracts/IDriveRegistry.sol +++ b/examples/contracts/IDriveRegistry.sol @@ -31,6 +31,12 @@ interface IDriveRegistry { uint32 validUntil; /// Provider-chosen replay-protection nonce. uint64 nonce; + /// `true` if the quote is bound to an existing bucket (`Some(_)` on + /// the Rust side) — required for replica terms; primary terms leave + /// this false. + bool hasBucketId; + /// Target bucket id; only read when `hasBucketId` is true. + uint64 bucketId; /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). bool hasReplicaParams; /// Replica funding parameters; only read when `hasReplicaParams` is true. diff --git a/examples/contracts/IS3Registry.sol b/examples/contracts/IS3Registry.sol index 439ebbcc..ca9bd936 100644 --- a/examples/contracts/IS3Registry.sol +++ b/examples/contracts/IS3Registry.sol @@ -29,6 +29,12 @@ interface IS3Registry { uint32 validUntil; /// Provider-chosen replay-protection nonce. uint64 nonce; + /// `true` if the quote is bound to an existing bucket (`Some(_)` on + /// the Rust side) — required for replica terms; primary terms leave + /// this false. + bool hasBucketId; + /// Target bucket id; only read when `hasBucketId` is true. + uint64 bucketId; /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). bool hasReplicaParams; /// Replica funding parameters; only read when `hasReplicaParams` is true. diff --git a/examples/contracts/IWeb3Storage.sol b/examples/contracts/IWeb3Storage.sol index d5edfd62..197e4e3d 100644 --- a/examples/contracts/IWeb3Storage.sol +++ b/examples/contracts/IWeb3Storage.sol @@ -2,13 +2,11 @@ pragma solidity ^0.8.34; /// @title IWeb3Storage -/// @notice ABI of the web3-storage precompile at -/// `0x0000000000000000000000000000000009010000` (matcher -/// `Fixed(0x0901)`). Mirrors -/// `precompiles/storage-provider-precompile/src/IWeb3Storage.sol`; -/// kept in sync manually. Substrate `AccountId32` is `bytes32`. The -/// EVM caller becomes the substrate-mapped owner via -/// `AccountId32Mapper`. +/// @notice Solidity interface for the web3-storage `pallet_storage_provider` +/// precompile (client-side bucket lifecycle). Substrate `AccountId32` +/// values (32-byte sr25519 public keys) cross the boundary as `bytes32`; +/// the EVM caller's substrate-mapped account is derived from +/// `msg.sender` via `AccountId32Mapper`. /// /// Role tags: 0 = Admin, 1 = Writer, 2 = Reader. interface IWeb3Storage { @@ -34,6 +32,12 @@ interface IWeb3Storage { uint32 validUntil; /// Provider-chosen replay-protection nonce. uint64 nonce; + /// `true` if the quote is bound to an existing bucket (`Some(_)` on + /// the Rust side) — required for replica terms; primary terms leave + /// this false. + bool hasBucketId; + /// Target bucket id; only read when `hasBucketId` is true. + uint64 bucketId; /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). bool hasReplicaParams; /// Replica funding parameters; only read when `hasReplicaParams` is true. diff --git a/examples/contracts/SharedTeamDrive.sol b/examples/contracts/SharedTeamDrive.sol index efdb9134..d1df4b2e 100644 --- a/examples/contracts/SharedTeamDrive.sol +++ b/examples/contracts/SharedTeamDrive.sol @@ -50,6 +50,8 @@ contract SharedTeamDrive { /// Create the team's drive by redeeming provider-signed terms /// (`terms.owner` must be the contract's substrate-mapped account). /// `msg.value` funds the agreement payment reserve held by that account. + /// Primary terms must not be bound to an existing bucket — the drive's + /// bucket is created at redemption. function createTeam( string calldata name, bytes32 provider, @@ -58,6 +60,7 @@ contract SharedTeamDrive { ) external payable returns (uint64) { require(admin == address(0), "team already created"); require(msg.value > 0, "must fund agreement"); + require(!terms.hasBucketId, "primary terms must not be bucket-bound"); admin = msg.sender; driveId = DRIVE_REGISTRY.createDrive(name, provider, terms, signature); emit TeamCreated(msg.sender, driveId); diff --git a/examples/contracts/StorageMarketplace.sol b/examples/contracts/StorageMarketplace.sol index 04410fac..c95b76d1 100644 --- a/examples/contracts/StorageMarketplace.sol +++ b/examples/contracts/StorageMarketplace.sol @@ -40,13 +40,15 @@ contract StorageMarketplace { /// Buy storage on behalf of `msg.sender` by redeeming provider-signed /// terms. The contract becomes the substrate-side bucket admin /// (`terms.owner` must be its substrate-mapped account); per-user - /// ownership is tracked here. + /// ownership is tracked here. Primary terms must not be bound to an + /// existing bucket — the bucket is created at redemption. function buyStorage( bytes32 provider, IWeb3Storage.PrimitiveAgreementTerms calldata terms, bytes calldata signature ) external payable returns (uint64 bucketId) { require(msg.value > 0, "msg.value must cover the agreement payment"); + require(!terms.hasBucketId, "primary terms must not be bucket-bound"); bucketId = WEB3_STORAGE.establishStorageAgreement( provider, terms, diff --git a/examples/contracts/TokenGatedDrive.sol b/examples/contracts/TokenGatedDrive.sol index ad351aac..b5a88b84 100644 --- a/examples/contracts/TokenGatedDrive.sol +++ b/examples/contracts/TokenGatedDrive.sol @@ -60,7 +60,8 @@ contract TokenGatedDrive { /// Bootstrap the bucket by redeeming provider-signed terms /// (`terms.owner` must be the contract's substrate-mapped account). - /// One-shot. + /// One-shot. Primary terms must not be bound to an existing bucket — + /// the bucket is created at redemption. function initialize( string calldata name, bytes32 provider, @@ -69,6 +70,7 @@ contract TokenGatedDrive { ) external payable returns (uint64) { require(publisher == address(0), "already init"); require(msg.value > 0, "must fund agreement"); + require(!terms.hasBucketId, "primary terms must not be bucket-bound"); publisher = msg.sender; s3BucketId = S3_REGISTRY.createS3Bucket(name, provider, terms, signature); emit Initialized(msg.sender, s3BucketId); diff --git a/examples/papi/api.js b/examples/papi/api.js index 2d6c43cb..b5018a6f 100644 --- a/examples/papi/api.js +++ b/examples/papi/api.js @@ -91,6 +91,7 @@ function buildSignedTermsArgs(provider, signed) { price_per_byte: BigInt(t.price_per_byte), valid_until: t.valid_until, nonce: BigInt(t.nonce), + bucket_id: t.bucket_id != null ? BigInt(t.bucket_id) : undefined, replica_params: t.replica_params ?? undefined, }; return { provider: provider.address, terms, sig }; diff --git a/examples/papi/sc-api.js b/examples/papi/sc-api.js index 2aebdc46..3e8fd25e 100644 --- a/examples/papi/sc-api.js +++ b/examples/papi/sc-api.js @@ -56,9 +56,11 @@ export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, d duration, price_per_byte: 0, replica_params: null, + bucket_id: null, }); const t = signed.terms; const rp = t.replica_params; + const bucket = t.bucket_id; return { terms: { owner: toHex(owner.publicKey), @@ -72,6 +74,8 @@ export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, d syncBalance: BigInt(rp?.sync_balance ?? 0), minSyncInterval: Number(rp?.min_sync_interval ?? 0), }, + hasBucketId: bucket != null, + bucketId: BigInt(bucket ?? 0), }, // Hex SCALE-encoded MultiSignature (variant byte + raw sig) — passed // through verbatim as the `bytes signature` ABI param. diff --git a/pallet/src/benchmarking.rs b/pallet/src/benchmarking.rs index 807ad239..0774a207 100644 --- a/pallet/src/benchmarking.rs +++ b/pallet/src/benchmarking.rs @@ -74,6 +74,7 @@ fn build_primary_terms( price_per_byte: 1u32.into(), valid_until: BlockNumberFor::::max_value(), nonce, + bucket_id: None, replica_params: None, } } @@ -81,6 +82,7 @@ fn build_primary_terms( /// Build replica [`AgreementTerms`] suitable for a benchmark agreement. fn build_replica_terms( owner: &T::AccountId, + bucket_id: BucketId, max_bytes: u64, duration: BlockNumberFor, nonce: u64, @@ -92,6 +94,7 @@ fn build_replica_terms( price_per_byte: 1u32.into(), valid_until: BlockNumberFor::::max_value(), nonce, + bucket_id: Some(bucket_id), replica_params: Some(ReplicaTerms { sync_balance: BalanceOf::::max_value() / 20u32.into(), min_sync_interval: 10u32.into(), @@ -143,8 +146,13 @@ fn setup_replica_agreement( replica_index: u32, ) { let key = register_sr25519_key::(replica, KEY_TYPE, replica_index); - let terms = - build_replica_terms::(admin, 1_000_000u64, 100u32.into(), replica_index as u64 + 1); + let terms = build_replica_terms::( + admin, + bucket_id, + 1_000_000u64, + 100u32.into(), + replica_index as u64 + 1, + ); let sig = sign_terms::(&key, &terms); Pallet::::establish_replica_agreement_internal(admin, bucket_id, replica, terms, &sig) .expect("establish_replica_agreement_internal succeeds"); @@ -500,7 +508,7 @@ mod benchmarks { let replica = create_provider::(1); let key = register_sr25519_key::(&replica, KEY_TYPE, 1); - let terms = build_replica_terms::(&admin, 1_000_000u64, 100u32.into(), 1); + let terms = build_replica_terms::(&admin, bucket_id, 1_000_000u64, 100u32.into(), 1); let signature = sign_terms::(&key, &terms); #[extrinsic_call] diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 26a01a23..4a0700e9 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -810,6 +810,10 @@ pub mod pallet { /// Replica terms missing from a signed quote redeemed as a replica /// agreement. MissingReplicaTerms, + /// The terms' bucket binding does not match the redeeming extrinsic: + /// primary terms must carry no bucket, replica terms must name the + /// targeted bucket. + TermsBucketMismatch, } // ───────────────────────────────────────────────────────────────────────── @@ -3758,6 +3762,10 @@ pub mod pallet { // Origin must match the owner the provider signed for. ensure!(&terms.owner == owner, Error::::TermsOwnerMismatch); + // Primary terms must not be bound to an existing bucket — the + // bucket is created at redemption. + ensure!(terms.bucket_id.is_none(), Error::::TermsBucketMismatch); + // Quote must not be stale. let current_block = frame_system::Pallet::::block_number(); ensure!(terms.valid_until >= current_block, Error::::TermsExpired); @@ -3865,6 +3873,13 @@ pub mod pallet { // Origin must match the owner the provider signed for. ensure!(&terms.owner == owner, Error::::TermsOwnerMismatch); + // The provider's signed quote must be bound to the bucket this + // extrinsic targets. + ensure!( + terms.bucket_id == Some(bucket_id), + Error::::TermsBucketMismatch + ); + // Quote must not be stale. let current_block = frame_system::Pallet::::block_number(); ensure!(terms.valid_until >= current_block, Error::::TermsExpired); diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index df85e733..58543166 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -60,6 +60,7 @@ fn primary_terms( price_per_byte, valid_until, nonce, + bucket_id: None, replica_params: None, } } @@ -68,6 +69,7 @@ fn primary_terms( #[allow(clippy::too_many_arguments)] fn replica_terms( owner: u64, + bucket_id: BucketId, max_bytes: u64, duration: u64, price_per_byte: u64, @@ -83,6 +85,7 @@ fn replica_terms( price_per_byte, valid_until, nonce, + bucket_id: Some(bucket_id), replica_params: Some(ReplicaTerms { sync_balance, min_sync_interval, @@ -1001,6 +1004,30 @@ mod establish_storage_agreement_tests { }); } + #[test] + fn rejects_primary_terms_bound_to_a_bucket() { + new_test_ext().execute_with(|| { + System::set_block_number(1); + let settings = default_test_settings(0, None); + let provider_pk = register_signing_provider(2, "//Provider", 200, settings); + + // Replica-shaped binding on a primary redemption. + let mut terms = primary_terms(1, 100, 100, 0, 1_000, 1); + terms.bucket_id = Some(0); + let sig = sign_terms(&provider_pk, &terms); + + assert_noop!( + StorageProvider::establish_storage_agreement( + RuntimeOrigin::signed(1), + 2, + terms, + sig, + ), + Error::::TermsBucketMismatch + ); + }); + } + #[test] fn rejects_expired_terms() { new_test_ext().execute_with(|| { @@ -1469,7 +1496,7 @@ mod establish_replica_agreement_tests { let owner_balance_before = Balances::free_balance(1); // payment = price 0 * 50 * 100 = 0; sync_balance = 25 is reserved. - let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let terms = replica_terms(1, bucket_id, 50, 100, 0, 10_000, 1, 25, 10); let sig = sign_terms(&replica_pk, &terms); assert_ok!(StorageProvider::establish_replica_agreement( RuntimeOrigin::signed(1), @@ -1511,7 +1538,7 @@ mod establish_replica_agreement_tests { new_test_ext().execute_with(|| { System::set_block_number(1); let replica_pk = register_replica_provider(3, "//Replica"); - let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let terms = replica_terms(1, 999, 50, 100, 0, 10_000, 1, 25, 10); let sig = sign_terms(&replica_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1532,8 +1559,10 @@ mod establish_replica_agreement_tests { let (bucket_id, _) = setup_primary_bucket(); let replica_pk = register_replica_provider(3, "//Replica"); - // Primary-shaped terms (no replica_params) cannot drive a replica. - let terms = primary_terms(1, 50, 100, 0, 10_000, 1); + // Terms without replica_params cannot drive a replica, even when + // bound to the right bucket. + let mut terms = primary_terms(1, 50, 100, 0, 10_000, 1); + terms.bucket_id = Some(bucket_id); let sig = sign_terms(&replica_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1554,7 +1583,7 @@ mod establish_replica_agreement_tests { let (bucket_id, _) = setup_primary_bucket(); let replica_pk = register_replica_provider(3, "//Replica"); - let terms = replica_terms(1, 10, 100, 0, 10_000, 1, 5, 10); + let terms = replica_terms(1, bucket_id, 10, 100, 0, 10_000, 1, 5, 10); let sig = sign_terms(&replica_pk, &terms); assert_ok!(StorageProvider::establish_replica_agreement( RuntimeOrigin::signed(1), @@ -1565,7 +1594,7 @@ mod establish_replica_agreement_tests { )); // Same provider, same bucket → duplicate agreement. - let terms = replica_terms(1, 10, 100, 0, 10_000, 2, 5, 10); + let terms = replica_terms(1, bucket_id, 10, 100, 0, 10_000, 2, 5, 10); let sig = sign_terms(&replica_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1588,7 +1617,7 @@ mod establish_replica_agreement_tests { let settings = default_test_settings(0, None); let replica_pk = register_signing_provider(3, "//Replica", 1_000, settings); - let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let terms = replica_terms(1, bucket_id, 50, 100, 0, 10_000, 1, 25, 10); let sig = sign_terms(&replica_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1609,7 +1638,7 @@ mod establish_replica_agreement_tests { let (bucket_id, _) = setup_primary_bucket(); let replica_pk = register_replica_provider(3, "//Replica"); // Terms signed for owner = 1, but origin = 4. - let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let terms = replica_terms(1, bucket_id, 50, 100, 0, 10_000, 1, 25, 10); let sig = sign_terms(&replica_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1624,6 +1653,28 @@ mod establish_replica_agreement_tests { }); } + #[test] + fn rejects_replica_terms_bound_to_another_bucket() { + new_test_ext().execute_with(|| { + let (bucket_id, _) = setup_primary_bucket(); + let replica_pk = register_replica_provider(3, "//Replica"); + + // Terms signed for a different bucket than the extrinsic targets. + let terms = replica_terms(1, bucket_id + 1, 50, 100, 0, 10_000, 1, 25, 10); + let sig = sign_terms(&replica_pk, &terms); + assert_noop!( + StorageProvider::establish_replica_agreement( + RuntimeOrigin::signed(1), + bucket_id, + 3, + terms, + sig, + ), + Error::::TermsBucketMismatch + ); + }); + } + #[test] fn rejects_expired_replica_terms() { new_test_ext().execute_with(|| { @@ -1631,7 +1682,7 @@ mod establish_replica_agreement_tests { let replica_pk = register_replica_provider(3, "//Replica"); System::set_block_number(50); - let terms = replica_terms(1, 50, 100, 0, 10, 1, 25, 10); + let terms = replica_terms(1, bucket_id, 50, 100, 0, 10, 1, 25, 10); let sig = sign_terms(&replica_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1653,7 +1704,7 @@ mod establish_replica_agreement_tests { let _replica_pk = register_replica_provider(3, "//Replica"); let (other_pk, _) = generate_provider_public_key("//Imposter"); - let terms = replica_terms(1, 50, 100, 0, 10_000, 1, 25, 10); + let terms = replica_terms(1, bucket_id, 50, 100, 0, 10_000, 1, 25, 10); let sig = sign_terms(&other_pk, &terms); assert_noop!( StorageProvider::establish_replica_agreement( @@ -1674,7 +1725,7 @@ mod establish_replica_agreement_tests { let (bucket_id, _) = setup_primary_bucket(); let replica_pk = register_replica_provider(3, "//Replica"); - let terms = replica_terms(1, 10, 100, 0, 10_000, 7, 5, 10); + let terms = replica_terms(1, bucket_id, 10, 100, 0, 10_000, 7, 5, 10); let sig = sign_terms(&replica_pk, &terms); assert_ok!(StorageProvider::establish_replica_agreement( RuntimeOrigin::signed(1), diff --git a/precompiles/drive-registry-precompile/src/IDriveRegistry.sol b/precompiles/drive-registry-precompile/src/IDriveRegistry.sol index 576583bd..3e34df15 100644 --- a/precompiles/drive-registry-precompile/src/IDriveRegistry.sol +++ b/precompiles/drive-registry-precompile/src/IDriveRegistry.sol @@ -31,6 +31,12 @@ interface IDriveRegistry { uint32 validUntil; /// Provider-chosen replay-protection nonce. uint64 nonce; + /// `true` if the quote is bound to an existing bucket (`Some(_)` on + /// the Rust side) — required for replica terms; primary terms leave + /// this false. + bool hasBucketId; + /// Target bucket id; only read when `hasBucketId` is true. + uint64 bucketId; /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). bool hasReplicaParams; /// Replica funding parameters; only read when `hasReplicaParams` is true. diff --git a/precompiles/drive-registry-precompile/src/lib.rs b/precompiles/drive-registry-precompile/src/lib.rs index 4f5e42fb..67b1046e 100644 --- a/precompiles/drive-registry-precompile/src/lib.rs +++ b/precompiles/drive-registry-precompile/src/lib.rs @@ -66,6 +66,7 @@ where price_per_byte: BalanceOf::::from(terms.pricePerByte), valid_until: BlockNumberFor::::from(terms.validUntil), nonce: terms.nonce, + bucket_id: terms.hasBucketId.then_some(terms.bucketId), replica_params: terms .hasReplicaParams .then(|| storage_primitives::ReplicaTerms { diff --git a/precompiles/s3-registry-precompile/src/IS3Registry.sol b/precompiles/s3-registry-precompile/src/IS3Registry.sol index 439ebbcc..ca9bd936 100644 --- a/precompiles/s3-registry-precompile/src/IS3Registry.sol +++ b/precompiles/s3-registry-precompile/src/IS3Registry.sol @@ -29,6 +29,12 @@ interface IS3Registry { uint32 validUntil; /// Provider-chosen replay-protection nonce. uint64 nonce; + /// `true` if the quote is bound to an existing bucket (`Some(_)` on + /// the Rust side) — required for replica terms; primary terms leave + /// this false. + bool hasBucketId; + /// Target bucket id; only read when `hasBucketId` is true. + uint64 bucketId; /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). bool hasReplicaParams; /// Replica funding parameters; only read when `hasReplicaParams` is true. diff --git a/precompiles/s3-registry-precompile/src/lib.rs b/precompiles/s3-registry-precompile/src/lib.rs index bf14656e..84d3b35a 100644 --- a/precompiles/s3-registry-precompile/src/lib.rs +++ b/precompiles/s3-registry-precompile/src/lib.rs @@ -69,6 +69,7 @@ where price_per_byte: BalanceOf::::from(terms.pricePerByte), valid_until: BlockNumberFor::::from(terms.validUntil), nonce: terms.nonce, + bucket_id: terms.hasBucketId.then_some(terms.bucketId), replica_params: terms .hasReplicaParams .then(|| storage_primitives::ReplicaTerms { diff --git a/precompiles/storage-provider-precompile/src/IWeb3Storage.sol b/precompiles/storage-provider-precompile/src/IWeb3Storage.sol index 9989eb7e..197e4e3d 100644 --- a/precompiles/storage-provider-precompile/src/IWeb3Storage.sol +++ b/precompiles/storage-provider-precompile/src/IWeb3Storage.sol @@ -32,6 +32,12 @@ interface IWeb3Storage { uint32 validUntil; /// Provider-chosen replay-protection nonce. uint64 nonce; + /// `true` if the quote is bound to an existing bucket (`Some(_)` on + /// the Rust side) — required for replica terms; primary terms leave + /// this false. + bool hasBucketId; + /// Target bucket id; only read when `hasBucketId` is true. + uint64 bucketId; /// `true` if the provider quoted replica terms (`Some(_)` on the Rust side). bool hasReplicaParams; /// Replica funding parameters; only read when `hasReplicaParams` is true. diff --git a/precompiles/storage-provider-precompile/src/lib.rs b/precompiles/storage-provider-precompile/src/lib.rs index b513ad0b..2ef71ea6 100644 --- a/precompiles/storage-provider-precompile/src/lib.rs +++ b/precompiles/storage-provider-precompile/src/lib.rs @@ -69,6 +69,7 @@ where price_per_byte: BalanceOf::::from(terms.pricePerByte), valid_until: BlockNumberFor::::from(terms.validUntil), nonce: terms.nonce, + bucket_id: terms.hasBucketId.then_some(terms.bucketId), replica_params: terms .hasReplicaParams .then(|| storage_primitives::ReplicaTerms { diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs index 3815b69c..d82b0f6a 100644 --- a/primitives/src/agreement_term.rs +++ b/primitives/src/agreement_term.rs @@ -31,6 +31,11 @@ pub struct AgreementTerms { /// Provider-chosen replay-protection nonce; uniqueness is enforced /// through the provider's sliding replay window. pub nonce: u64, + /// Bucket the quote is bound to. + /// - `None` for primary terms + /// - `Some(id)` for replica terms — must match the bucket targeted by + /// the extrinsic. + pub bucket_id: Option, /// Replica-specific parameters. /// - `None` means these are primary terms; /// - `Some(_)` means the provider has quoted a replica agreement and the extra per-sync funding is included. diff --git a/provider-node/src/api.rs b/provider-node/src/api.rs index 2718c96b..e275f6cc 100644 --- a/provider-node/src/api.rs +++ b/provider-node/src/api.rs @@ -756,6 +756,7 @@ async fn negotiate_terms( // TODO: lookup current block and use a bounded offset. valid_until: u32::MAX, nonce: nonce_counter.next(), + bucket_id: req.bucket_id, replica_params: req.replica_params, }; let signature = negotiate::sign_terms(keypair, &terms); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 3574d6bd..532c5031 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -122,11 +122,6 @@ pub type SignedBlock = generic::SignedBlock; pub type BlockId = generic::BlockId; /// The SignedExtension to the basic transaction logic. -/// -/// The trailing `pallet_revive::evm::tx_extension::SetOrigin` lets the runtime -/// accept Ethereum-signed transactions via `pallet_revive::eth_transact` — -/// `SetOrigin` recovers the signer's `H160` from the signature and maps it to -/// a substrate `AccountId32` (see [`crate::revive::EthExtraImpl`]). pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< Runtime, ( @@ -139,6 +134,7 @@ pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, + // lets the runtime accept Ethereum-signed transactions via `pallet_revive::eth_transact` pallet_revive::evm::tx_extension::SetOrigin, ), >; diff --git a/runtime/src/revive.rs b/runtime/src/revive.rs index 790c2ed1..2585ccbb 100644 --- a/runtime/src/revive.rs +++ b/runtime/src/revive.rs @@ -79,7 +79,7 @@ impl EthExtra for EthExtraImpl { frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), pallet_revive::evm::tx_extension::SetOrigin::::new_from_eth_transaction(), ) .into() diff --git a/runtimes/web3-storage-paseo/src/lib.rs b/runtimes/web3-storage-paseo/src/lib.rs index a0abcb4a..e5a78fa8 100644 --- a/runtimes/web3-storage-paseo/src/lib.rs +++ b/runtimes/web3-storage-paseo/src/lib.rs @@ -124,11 +124,6 @@ pub type SignedBlock = generic::SignedBlock; pub type BlockId = generic::BlockId; /// The SignedExtension to the basic transaction logic. -/// -/// The trailing `pallet_revive::evm::tx_extension::SetOrigin` lets the runtime -/// accept Ethereum-signed transactions via `pallet_revive::eth_transact` — -/// `SetOrigin` recovers the signer's `H160` from the signature and maps it to -/// a substrate `AccountId32` (see [`crate::revive::EthExtraImpl`]). pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< Runtime, ( @@ -141,6 +136,7 @@ pub type TxExtension = cumulus_pallet_weight_reclaim::StorageWeightReclaim< frame_system::CheckWeight, pallet_transaction_payment::ChargeTransactionPayment, frame_metadata_hash_extension::CheckMetadataHash, + // lets the runtime accept Ethereum-signed transactions via `pallet_revive::eth_transact` pallet_revive::evm::tx_extension::SetOrigin, ), >; diff --git a/runtimes/web3-storage-paseo/src/revive.rs b/runtimes/web3-storage-paseo/src/revive.rs index 1668b21a..126a7598 100644 --- a/runtimes/web3-storage-paseo/src/revive.rs +++ b/runtimes/web3-storage-paseo/src/revive.rs @@ -79,7 +79,7 @@ impl EthExtra for EthExtraImpl { frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), pallet_revive::evm::tx_extension::SetOrigin::::new_from_eth_transaction(), ) .into() diff --git a/runtimes/web3-storage-paseo/tests/tests.rs b/runtimes/web3-storage-paseo/tests/tests.rs index a1393910..738332d5 100644 --- a/runtimes/web3-storage-paseo/tests/tests.rs +++ b/runtimes/web3-storage-paseo/tests/tests.rs @@ -63,7 +63,7 @@ fn construct_extrinsic( }), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(0u128), - frame_metadata_hash_extension::CheckMetadataHash::::new(true), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), // SetOrigin's substrate-signed path is a no-op; only the eth path // (built by `EthExtraImpl::get_eth_extension`) sets `is_eth_transaction`. pallet_revive::evm::tx_extension::SetOrigin::::default(), @@ -155,6 +155,7 @@ fn primary_terms( price_per_byte: 0, valid_until: 1_000_000_000, nonce, + bucket_id: None, replica_params: None, } } diff --git a/storage-interfaces/file-system/client/examples/basic_usage.rs b/storage-interfaces/file-system/client/examples/basic_usage.rs index 32d57154..8d0cd8af 100644 --- a/storage-interfaces/file-system/client/examples/basic_usage.rs +++ b/storage-interfaces/file-system/client/examples/basic_usage.rs @@ -69,6 +69,7 @@ async fn main() -> Result<(), Box> { duration: 500, // 500 blocks price_per_byte: 1, replica_params: None, + bucket_id: None, }, ) .await?; diff --git a/storage-interfaces/file-system/client/examples/ci_integration_test.rs b/storage-interfaces/file-system/client/examples/ci_integration_test.rs index 172af081..f381284f 100644 --- a/storage-interfaces/file-system/client/examples/ci_integration_test.rs +++ b/storage-interfaces/file-system/client/examples/ci_integration_test.rs @@ -86,6 +86,7 @@ async fn main() -> Result<(), Box> { duration: 500, // 500 blocks price_per_byte: 0, replica_params: None, + bucket_id: None, }, ) .await?; diff --git a/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs b/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs index a20e1aa6..e3521e30 100644 --- a/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs +++ b/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs @@ -107,6 +107,7 @@ fn make_primary_terms(owner: &T::AccountId, nonce: u64) -> AgreementT price_per_byte: 1u32.into(), valid_until: BlockNumberFor::::max_value(), nonce, + bucket_id: None, replica_params: None, } } diff --git a/storage-interfaces/file-system/pallet-registry/src/tests.rs b/storage-interfaces/file-system/pallet-registry/src/tests.rs index 730b23e5..864716f5 100644 --- a/storage-interfaces/file-system/pallet-registry/src/tests.rs +++ b/storage-interfaces/file-system/pallet-registry/src/tests.rs @@ -39,6 +39,7 @@ fn primary_terms(owner: u64, max_bytes: u64, duration: u64, nonce: u64) -> Agree price_per_byte: 0u128, valid_until: 1_000_000u64, nonce, + bucket_id: None, replica_params: None, } } diff --git a/storage-interfaces/s3/client/examples/basic_usage.rs b/storage-interfaces/s3/client/examples/basic_usage.rs index 158d3724..c0161166 100644 --- a/storage-interfaces/s3/client/examples/basic_usage.rs +++ b/storage-interfaces/s3/client/examples/basic_usage.rs @@ -58,6 +58,7 @@ async fn main() -> Result<(), Box> { duration: 500, // 500 blocks price_per_byte: 0, replica_params: None, + bucket_id: None, }, ) .await?; diff --git a/storage-interfaces/s3/client/examples/ci_integration_test.rs b/storage-interfaces/s3/client/examples/ci_integration_test.rs index 493c7837..f7198cc1 100644 --- a/storage-interfaces/s3/client/examples/ci_integration_test.rs +++ b/storage-interfaces/s3/client/examples/ci_integration_test.rs @@ -63,6 +63,7 @@ async fn main() -> Result<(), Box> { duration: 500, // 500 blocks price_per_byte: 0, replica_params: None, + bucket_id: None, }, ) .await?; diff --git a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs index 58cd0ed4..2642c98f 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs @@ -230,6 +230,7 @@ mod benchmarks { price_per_byte: 1u32.into(), valid_until: BlockNumberFor::::max_value(), nonce: 1, + bucket_id: None, replica_params: None, }; let sig = sign_terms::(&provider_pk, &terms); diff --git a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs index 878c0150..e8808cdf 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs @@ -43,6 +43,7 @@ fn primary_terms(owner: u64, max_bytes: u64, duration: u64, nonce: u64) -> Agree price_per_byte: 0u128, valid_until: 1_000_000u64, nonce, + bucket_id: None, replica_params: None, } } diff --git a/user-interfaces/console-ui/src/components/S3Tab.tsx b/user-interfaces/console-ui/src/components/S3Tab.tsx index 23d8b685..d85466a4 100644 --- a/user-interfaces/console-ui/src/components/S3Tab.tsx +++ b/user-interfaces/console-ui/src/components/S3Tab.tsx @@ -267,6 +267,7 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { duration: parseInt(bucketDuration, 10), price_per_byte: BigInt(bucketPricePerByte || "0"), replica_params: null, + bucket_id: null, }); } catch (err) { const msg = err instanceof Error ? err.message : "Failed to negotiate with provider"; diff --git a/user-interfaces/console-ui/src/lib/storage.ts b/user-interfaces/console-ui/src/lib/storage.ts index 0c749323..17199daa 100644 --- a/user-interfaces/console-ui/src/lib/storage.ts +++ b/user-interfaces/console-ui/src/lib/storage.ts @@ -47,6 +47,7 @@ export interface SignedTerms { valid_until: number; nonce: number | bigint; replica_params: unknown | null; + bucket_id: bigint | null; }; signature: string; } @@ -57,6 +58,7 @@ export interface NegotiateRequest { duration: number; price_per_byte: number | bigint; replica_params: unknown | null; + bucket_id?: bigint | null; } /** @@ -157,6 +159,7 @@ export function buildSignedTermsArgs( valid_until: t.valid_until, nonce: BigInt(t.nonce), replica_params: (t.replica_params ?? undefined) as any, + bucket_id: t.bucket_id ? BigInt(t.bucket_id) : undefined, }; return { provider: providerAccount, terms, sig }; } diff --git a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx index fff5457d..105a25a0 100644 --- a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx +++ b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx @@ -140,6 +140,7 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro duration: parseInt(duration, 10), price_per_byte: BigInt(pricePerByte || "0"), replica_params: null, + bucket_id: null, }); } catch (err) { setNegotiateError( diff --git a/user-interfaces/drive-ui/src/lib/drive-client.ts b/user-interfaces/drive-ui/src/lib/drive-client.ts index 8cb642ce..103b2cb6 100644 --- a/user-interfaces/drive-ui/src/lib/drive-client.ts +++ b/user-interfaces/drive-ui/src/lib/drive-client.ts @@ -41,6 +41,7 @@ export interface SignedTerms { valid_until: number; nonce: number | bigint; replica_params: unknown | null; + bucket_id: bigint | null; }; signature: string; } @@ -51,6 +52,7 @@ export interface NegotiateRequest { duration: number; price_per_byte: number | bigint; replica_params: unknown | null; + bucket_id?: bigint | null; } export interface AvailableProvider { @@ -229,6 +231,7 @@ export function buildSignedTermsArgs( nonce: BigInt(t.nonce), // eslint-disable-next-line @typescript-eslint/no-explicit-any replica_params: (t.replica_params ?? undefined) as any, + bucket_id: t.bucket_id ? BigInt(t.bucket_id) : undefined, }; return { provider: providerAccount, terms, sig }; } diff --git a/user-interfaces/shared/papi/.papi/descriptors/package.json b/user-interfaces/shared/papi/.papi/descriptors/package.json index d60a187d..902965e4 100644 --- a/user-interfaces/shared/papi/.papi/descriptors/package.json +++ b/user-interfaces/shared/papi/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.16173887892502261417", + "version": "0.1.0-autogenerated.18330026386231391718", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/user-interfaces/shared/papi/.papi/metadata/parachain.scale b/user-interfaces/shared/papi/.papi/metadata/parachain.scale index 32480361c70c3d3030a5d537a9f25049446736d5..b232ddbf9cc74ca79164d6c3429747259d2e4254 100644 GIT binary patch delta 6332 zcma)A4^&j=m4Dy;24>y}2`K0wpu<0bRFqLc5Ku%=5fBZaF-mY89x%ep!2D53M8y;p z4O$w>6;onp%h|P+J+WOfhqh!(Pe?;|si9l5@tnA=n{`h;t=rHOPYx$#yZ5~}gc;}T zIeX57@6Gq!`@4U>d+&F@clJT}Z$1kTwMlMsL~dv3NGSVB5;Nj>66;DyKH{-7OYS;r z{VcFUm91{8!=7((n%&Z-O-0I5Y5itzaWebIFIV3TW%UO=IeEsS((1a3t$XVBm+mgE z++SB!`eI3~AlwWU1b#*kgfL+^6ip`}290qL&u-)9IB>Bs{81ciX5%;$2V2-An&KgY z)}|SlmX3sKssuqJjD|A9KFN|_y;%@O7BF=9%Xr8VM@ayTgbHH{6K1b~#p&ar8iT5} zMpMluLYXdNx5F-x77GdG`YZKL{55fKW-WJZNS?e}I>gU=cHUk0!ysu+!z$ zlGEl=x2?kJvYFkMCZQ}WgjF|5Mz_3{YE<{zc*JV2x7r(x^$y8pv^(5Jh1lpOLZ?(O zNj7;=I^lL&?Jlcj(_dSiR-4(`uA&%i9+%r_F*|9(?(jjbHh#k?wYFF-W`82P*+!z9 zq(ZmZ*(kZCdLK1L%&;V2TKvzo(8ogX7z`nFo8DQ-~Mo~*#wUn zi^R(HaEL|Yt@UskqGjr$uo$iE=d^gO5U)(I1blA;9A+svm`z)^Aq$qWbS%rFpv%Da zELews&Vpk&5DkkVLlGEl(yCEdpk0VU6TX}ct5}wdvYO@Mud^WyvJ?rBs|@-Ak_;xT zuvLrfUs$|%Xd~!>mEb$M(8)@1-6r@34KHnib*ut=H$e{Di@(|gMjGgs5f|k_G^@dc zJj#_CdCGwM^I#4RsCLw()h*_6LFvV+SZ8z!`=RJ#gE zB@Z?gLMlNjqTelqovZ`@RtT|ThirJKR_IihO1Hx0=q@cQu6CO3F0(~0B~{i&yHt;+ zZIF)9MdbT#+*SnZ`Jk5JOGU60uN09|H}Quw$wu&ZMUZ72)3QT;5am*%+0w28QHc<` zWohfMVH>2e9=x&*vPs1cwm~xeQdC^A1Hv%A7~)tjZYqXE){nKtu!Gz%R1BBJURmot zt z45PMGB@DySB3Oi%caqbF@z%}-#fDYbHKhvg{!-dDMVS|={(V#iDVVs6qVqQ9?jlEz zpm`VV-6;NS*VhQRs2tYgt}?|YnSZ6EWl=@#ZpkIED7(XMk-9mHsG*G%n9l7`VDqTt zE;wX!ImPcdHkU&Y4a)p)%SoIE|5^^Y{~x?~Y^#8HaYFXhq*j zwGsHIN{Gke-B8T#$`n!fKb7Q_zuyfdR7U@MH|?`B#N(Dqs;oAAy%Gx86i!!yfmY@@ zt}ef?Wk&fd@R+hU3oOm;l%zVF+12d2kGJ*^?R}is17+fUS#&8E9$?H~NMR3g%U&2@ zkG%KyLaTvF>+YjeDrWG9Mk!hc3skTlXrC6NBua44I%;ENCNPm&({7;fOZO}vg z>RB@6g6gtLS*jWb9qu~Sy*-|WY z!HINp0FD%lBdItNK5!{TbhVq(!-fakkZNj^weHdgo&b>cV32kdq}_X)*dVV%=Ihi6 z^pLe0TI~uz=nh8cRuQ_95LRLCaXKSPCucG#=s1HuSk14bI+F|Xp12P;f56a_^4uAeKq~TZX@B!6eMTI}S3~q5)UcIdo zZmV;bUZG@Z$I(}yj;d44Nm!Xa5}?9punME93ZrN~3EeqkM81$L<8s7J5RZ~9<3u?h zaT6++JOxjHnzqCa7&tu{!2O9o^Cv;PQvt;9`xCz(L_8fpJO=*MW8fnXqb&9*c2|dk zkJ&ObpMn^2=_{weM0IHR6lLZE{MS>EPW5L_{&f#$I;sE7KMi|`pyxE~CW22-LnS=a z;lJ;NrD29jr?t^)m%G+SSb2ux=aCQm${7eVJr2-pM)p{===0c&pU0v_CDCSd$Xwh%nrN#QPHPbX~1d=emPRu(lk zzd5shqUO}inUzhmP}QtC3u~4WfsQ4&I$U&xus1knbu6NkMmm+S?jyELFECON?r|Xm z-#ZJbv7-DlTomY`9Qh(r6zCD4g9*`Zyb4vBF`|E@#EbrR#}nmzr1s2TN zGUt!4($TA0wI(3|NG{JQ^ZxUz&M5>*bt(m$zX>(eAC7+$YL{dLuzpCaCNy=y5}iq< zHeq=eI89jrWS&r>%bgeDp;mof)m)V;SH$rUI_sm(LB^7T0A5){t}4PuycHuZKpb8= z2aBnbeCr%kQ}5W>O`EF>aaed>X&}w#$>I{3I2Ny;hvn30j+}=C8vgA(b%ho3N`2UVEz9)XRpp+Cf;*zY>98qn zPgHJLquCWqkxa97bd9k{!D~@gd#lIYhM!)7l%(M0e4F`1T?6$Wt&+1&2FK*rNHGsq zz6Q&d2Gh@XV6lhV#jh2WyERfY>X^C36Rd(@B}Xk>WP5|#54mrpP_>cMK++0rK2I5X zsCD$<+8$UYc|^9s=N6?mq+7Hn0G7JOr%!5vl408e=sGBlpE}Vy)F|#y#4f{M^gwiG zXTaum$(!3fp8;Kdo7=7OcB$c!cp0wIJwS=sk1sUV-Y!UaDF?8`9FrfL^?M z1yVBm0z~x(#8JPWC|?NotMHm{L3QRp0IP?SmV@(JdMM)O^K?+va!_Uc-nT%a&Of4` z4oFk4a!IM^g|*bBcl6SoU=)AU3(4s>1Kc(o?6zSQbXWyS{Wfe#za79c63jEA@{FiF z-fzRf^w9vGv0$Dtm1j)lvAhm|bUGf|S$>2TVxsSgr1*mNw% zw}Vyk_k%_`>gnWZaZQUXM?R6Z(GNw4oVYEHM%P^&>w{(S!I_jS^VPZATc!Ir(+8^} zr)8t2MB%PH-V=p;s)~EwfLyv$U3`N!>b^Sl>o?#9GEcF%66xmj5bq$QEEWV~v9+P0 z!ck9cVOXnEQs!5MM#zVl@}6dVC~AyF)pX;iFt_S7Wvr8H_OVAIT|Z|@uJA}j-Exht z+G-I%&ow0tU%N&=eT;v14U%>|ehNg=5{O3znUO){fAB;U=%MYAw6r3F!<;D4LsOlc zBr-il_QUmDKWDFYxt$)1yH0joh@S0K&iMJgt8z=Nlr6-8 zs25t~6P^ecI?&00QYF*O7lCeiC-U?F@(exI3=^rX-tJ^wd{-oss(6N?(fc`qiF|f} z`ml-bz-q|SE8U`3KL9!a`fT@kD~rR-8+3AqibtwHbm(1N{S2?9@ICnqFQef4@EQJu&3NB?mQOPF#Csu<4~9}Z_MCwq zlRv0^*TC0O6Z9Jc|BS@E8^hPot@B|F-%0n$HOqMgt({!XSMeBaf_G>+f0mIJBeDD| zI$(8TkD?11-hJTGRf);qL<|AlGH+Q@J(X}6i9@ofn8{w|5{2R7y1xQZt+ zy6PWHggsMqf5uP^7-dc z89JT&#JDEy2(`A&bxmehQ@+Xj-2y%dEZ5so$c>B@V9!?G&q}?iMf@@&>)zeQ6X@3Y z$u@p6v{o1K9pg5fE9R@bXNvg|-6E?lO=89(kF}*9pRMEuZ%74+XEyK0yZK?x+PoW| z=N3j;|7I0g(1E|L;@epl##ZxY)Zf%a zwODOdcg`>#+s8)$OKEMb0io3c9xe6M~ zoyzA1xq)}p9h00cD%ClU(OJuLS&a8qE&n;tM^D>f{$_Z(Xq20MxtM>Efzf6@{M6JP zGyfZX)VyillAX3@h&#t&9+js(s=eL1(kM492X5C3_oq*%kfDw hf53;t1iaV6zgoZ;5>vdtw{as-BhFg+DW0Vl{ujUDr)2;D delta 6157 zcmZ`-4Nz29mVWo#ru)4XVz+`eA_xN7odiRx7?7XHPy3JJ4;nQN>0qlJ=x*pA6~{kJ z@MjY=n!zJcDpDI$>tx7=nUF5CVK-z#HtdEOo!#t?yW>pQ6jSBQk`39gTV&SFp8Fnz zZaZ7`;J&{1obUYHbIv{Q{_69DlS2u5k1Oaji zqn9M;)=M{4ay*;{Gi<$jon>P~(Czc; zbX!@Up2?;SKCerXB-0k3$EB0{h>1y3ANHidkLUH%FHV2bfL=V3PR=iq2Gtj9B={a5}un?%>`Gn7{l^l(f6C((V!zNfU!kF3OjN=mm!L(y)g7c?NvI z?&0h6p~X0&(&((xsA^7%RWp7p!uT=4ZI#BcITIdbIpH@mVOu=fO)wws>+v6Qp^Y(g z+o6eu&+V`f;tVg_Z{;R_50nzFS_~tMC1A-?c#4_v%2GHEWrGspBz~9nTQjS40pujM-dpYQSoVM96 za2{oO*!wtG!LIS|Vwe{0$Oi_j6klBq&CG!rh42Usm4&dF zt-%w8kk2adW+9~G@j{BTZweuaZ9-EKq|!jk7F<~b_n}&qz;=seDz^Ha-hi`K#P=5WKCf$!gr+jEqOP3wq6HV1!xGkkduia;4Gf#hDYJfB z4k@e~e^Cy3{3cCiR6?>&CrK?40-jw3@mO5}R(2MTRlqV*vZn&FaDD|WyyT$iRFDUsww}@e2nmRa%9<7Yx#c2)Iov!J)K?g?59~9Pj7%!rBC>$-yroz zsPU5xP%~{nEDRc?L4yhZwTfbF$wsJPw*^@uj#QD3xDgywip*m1n~h|V`rg3Yc(MwL z*#N#@1s0l|}$I|KVC7Vzy&uui!nlv%^2 zyC`plY&L|8HbVyt8?fOQ%n}~n4BIVm&w%MWSt4$(gH@R$2392`ZESEhhFp$ALBHD@ zaMx1p9uZKZ1}bpx)ls;P;UDT~f5tJd9&F|J4UvU=+z?Z^$BCQLh{*(!FU83^%Vt*~ zR3DV7&gbZK4yIsdJtWEs5fGh%*L*M^e^*cSCIR&xn4e;n+3IRm{#XM?O;$F4KoHfEvYDU}4O{~$w_ z=%LbioffjO&%naT3TI-O(}kW#$E z@7GBa_SiKQ*EZ1UlZWFCu&c0u5T?$$Vq$*atSi=BAqFUwC3;9V%V3=3!1Ik%!q#Aw zA2M;M5xi7D>3uEL%PW3ZY}*tCT^$QutwC372+0B1Lv{670NSafT7q!iwmS;J8H?c5 z5S(}`s21Fm5T%bBSBGGs-6J$^(MgR_AfZ^0kOmUM?jOQ}@JAs!CJLJb^HEu%hs@Ox zNploTODs%_2GgR#p!E=)D`#=nAvj6-{M$p2ubdT_t+LdrQ8=2Q$aWzLwJjF4O+#(d zcy2Yp&nP9IKMaqV+XWmgcZji5mO3?(KOBaS*md##igH6Nb;(kfwsiOiWlRYF;t14G z-I9;OJX?2^{GM3(J(~O;4dK91sLj7gJX2}YD}t_%7}cQbCGyFj>(f}p6pR6NOXg$H zF|$94``3aQ`;Ng(*97@M)cS{!;2*|d4@MF9LL_xB#E{>XUruGSvFbP^lNX;o4t8q7 zt{$iSyo(XMMxUg zwq;D%SWA_&;yG9}n+Rl<)!+*RYuw(wzA>4Z*3gJg2iY8>bYE=Y9qK@^>p57M5=Xj% zqDb^ma(tbjNc2#;Fv*Pn@f>W)O;REwBw2|xI+;i(BV@wpWWkk!t5{*s`b~icVnXeGm2oPceRb2Ik`7(=dbD&(_nhm5OHP zpV8F+o`yv2y-3u~Gh}zEAWp%vXJ8I>r>~uX3>rQ5z5~X76cK*wP7$ zg`5EOG>uV!&R9SK5rCm6x+aRa2hQpyEl8V$KnuRV0^;zUb70Cn8nt!JV(VHasmDaW^MD$=ixGa52*3je;!uQai-#?y#!m$tqQYFm|?Y%4XyaxOOTy=AxctPRFJeq zNScVwHVyqh2{E@liZ#SZ$&N`SLln%D$=;zU*`cvsyZ|oh{Z++G7b&o=W6?!;jGF5D zi}Y2{gPj*4%XU4=b2nl=cS8fc@g0x}XT1zh*t((^yJH! zH#Nqa5saIE1izfwOY_jDNc2!~&F@#Jx1Y4mgip1BiCS|{8{jnUhnI*$sRPD&yI^&cb~C3;ln(MoC*a(`?pQI1IU;`}GVsm8QF zs>}(R$qLd!??j#*MQ$@v!O0=D)jrlHbeZ^g%Fz@TQ#$#>d@09x=`75_w>zl!vWJH{ zARTn$dUh3?=g~gQy+#L~f-9~;KG6NsxkhR@cU_~ap;xcLG&UT5;~MBZ>>7?wi&cl;O5+ub?Ff&}<6kk|Zi6`?Jet8v_@z$Jhab!4+kp*)PcP(|j4lw~ zu<~r(0pO&403- zJ9PIArq}Qm&tTd^-?>S0Hhxjezf82r za_~@}&iV}XzI8R?exWEQ{8b4b0G1biuau`VR)BZQcsp~150>+DjEvJecm{Prk7*oO1>a`%fX+Kr@3USi=~%^-1U2~rixp_)f-4WbB7mH@uxZSgimkbwTu$~ zk6X!tCd{hlD_JwvRrANmo@PcwJ7ccYEBy z{2Tbuc7BrpPCUu~if*incJPCAMg8^;a@znF?&N>V2E%+8@8oH>Ws@k5>FfRKorLJ{ z12qR+{s7hW{CoJp4|oAf3jfa!`1?SYsl85qB_Uf$7heOS@_%hZ*E789fvI`5{9C#z zowtYYB@dn2L$2A4NiO~_?dUBR#RA2|C;Rx1O`8 Date: Thu, 4 Jun 2026 12:58:38 +0700 Subject: [PATCH 34/44] feat: validate negotiate requests against on-chain provider settings The /negotiate handler blindly signed whatever terms the client proposed (including price_per_byte=0) and the on-chain extrinsic trusts that signature as provider consent. It was also an unauthenticated nonce/CPU burner. - Fetch the provider's on-chain registration info at startup (ProviderClient::get_provider_info) and store it in ProviderState; expose it via /info - Reject terms below the listed price, outside duration bounds, beyond remaining capacity, or against closed acceptance flags - before a nonce is allocated or anything is signed - Add typed rejection errors (422) plus provider_info_unavailable (503) and rate_limited (429) - Rate-limit /negotiate (5 req/s, burst 16) via tower RateLimit+Buffer - Enable checkpoint coordinator in just start-provider and CI --- .github/workflows/integration-tests.yml | 7 +- Cargo.lock | 2 + client/src/discovery.rs | 2 +- justfile | 1 + provider-node/Cargo.toml | 1 + provider-node/src/api.rs | 40 +++++- provider-node/src/command.rs | 46 +++++- provider-node/src/error.rs | 96 +++++++++++++ provider-node/src/lib.rs | 12 +- provider-node/src/negotiate.rs | 178 ++++++++++++++++++++++++ provider-node/src/types.rs | 3 +- 11 files changed, 376 insertions(+), 12 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e20b9db9..692f91c0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -145,7 +145,7 @@ jobs: nohup ./target/release/storage-provider-node \ --keyfile /tmp/alice-key --storage-mode inmemory \ --bind-addr 0.0.0.0:3333 --chain-rpc ws://127.0.0.1:2222 \ - > /tmp/provider.log 2>&1 & + --enable-checkpoint-coordinator > /tmp/provider.log 2>&1 & echo "Waiting for provider HTTP server..." for i in $(seq 1 60); do if curl -s http://127.0.0.1:3333/health | jq -e '.status' > /dev/null 2>&1; then @@ -166,7 +166,7 @@ jobs: nohup ./target/release/storage-provider-node \ --keyfile /tmp/charlie-key --storage-mode disk --storage-path /tmp/provider-data \ --bind-addr 0.0.0.0:3334 --chain-rpc ws://127.0.0.1:2222 \ - > /tmp/provider-disk.log 2>&1 & + --enable-checkpoint-coordinator > /tmp/provider.log 2>&1 & echo "Waiting for disk provider HTTP server..." for i in $(seq 1 60); do if curl -s http://127.0.0.1:3334/health | jq -e '.status' > /dev/null 2>&1; then @@ -342,8 +342,7 @@ jobs: nohup ./target/release/storage-provider-node \ --keyfile /tmp/alice-key --storage-mode inmemory \ --bind-addr 0.0.0.0:3333 --chain-rpc ws://127.0.0.1:2222 \ - --enable-agreement-coordinator --enable-checkpoint-coordinator \ - > /tmp/provider.log 2>&1 & + --enable-checkpoint-coordinator > /tmp/provider.log 2>&1 & for i in $(seq 1 60); do if curl -s http://127.0.0.1:3333/health | jq -e '.status' > /dev/null 2>&1; then echo "Provider is healthy (attempt $i)" diff --git a/Cargo.lock b/Cargo.lock index 60219a47..0c759b93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9743,6 +9743,7 @@ dependencies = [ "subxt-signer", "thiserror 2.0.18", "tokio", + "tower", "tower-http", "tracing", "tracing-subscriber 0.3.19", @@ -10438,6 +10439,7 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/client/src/discovery.rs b/client/src/discovery.rs index 5e5c6efa..1500b02c 100644 --- a/client/src/discovery.rs +++ b/client/src/discovery.rs @@ -64,7 +64,7 @@ pub struct MatchedProvider { } /// Provider information for discovery. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ProviderInfo { /// Network address for connecting. pub multiaddr: String, diff --git a/justfile b/justfile index 4c274cf0..a9ae5524 100644 --- a/justfile +++ b/justfile @@ -147,6 +147,7 @@ start-provider MODE="inmemory" PORT=PROVIDER_PORT STORAGE_PATH="./provider-data" --storage-mode "{{MODE}}" \ --bind-addr "0.0.0.0:{{PORT}}" \ --chain-rpc "{{ CHAIN_WS }}" \ + --enable-checkpoint-coordinator \ $EXTRA_ARGS # Register provider on-chain (idempotent). Requires a running chain. diff --git a/provider-node/Cargo.toml b/provider-node/Cargo.toml index d7d91b18..7ed0032c 100644 --- a/provider-node/Cargo.toml +++ b/provider-node/Cargo.toml @@ -12,6 +12,7 @@ storage-client = { workspace = true } storage-primitives = { workspace = true, features = ["serde", "std"] } tokio = { workspace = true } axum = { workspace = true } +tower = { workspace = true, features = ["buffer", "limit", "util"] } tower-http = { workspace = true } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true } diff --git a/provider-node/src/api.rs b/provider-node/src/api.rs index e275f6cc..3769c87b 100644 --- a/provider-node/src/api.rs +++ b/provider-node/src/api.rs @@ -12,6 +12,7 @@ use crate::storage::{hex_decode, hex_encode}; use crate::types::*; use crate::ProviderState; use axum::{ + error_handling::HandleErrorLayer, extract::{DefaultBodyLimit, Query, State}, routing::{get, post, put}, Json, Router, @@ -20,11 +21,19 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use codec::Encode; use sp_core::H256; use std::sync::Arc; +use std::time::Duration; use storage_primitives::AgreementTerms; use storage_primitives::{CheckpointProposal, CommitmentPayload}; +use tower::{buffer::BufferLayer, limit::RateLimitLayer, BoxError, ServiceBuilder}; use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; +/// `/negotiate` rate limit +const NEGOTIATE_RATE_LIMIT_PER_SEC: u64 = 5; +/// Requests queued while the limiter is saturated; beyond this they fail +/// fast with 429. +const NEGOTIATE_RATE_LIMIT_BURST: usize = 16; + /// Create the API router with all endpoints. pub fn create_router(state: Arc) -> Router { Router::new() @@ -51,7 +60,22 @@ pub fn create_router(state: Arc) -> Router { .route("/mmr_subtree", get(get_mmr_subtree)) .route("/fetch_nodes", post(fetch_nodes)) // Off-chain term negotiation (signed AgreementTerms for `establish_storage_agreement`) - .route("/negotiate", post(negotiate_terms)) + .route( + "/negotiate", + post(negotiate_terms).layer( + // RateLimit isn't Clone, so it needs a Buffer in front; + // Buffer's BoxError is turned back into a response here. + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + Error::RateLimited + })) + .layer(BufferLayer::new(NEGOTIATE_RATE_LIMIT_BURST)) + .layer(RateLimitLayer::new( + NEGOTIATE_RATE_LIMIT_PER_SEC, + Duration::from_secs(1), + )), + ), + ) // Checkpoint coordination .route("/checkpoint/sign", post(sign_checkpoint_proposal)) .route("/checkpoint/duty", get(get_checkpoint_duty)) @@ -125,6 +149,10 @@ async fn health() -> Json { async fn info(State(state): State>) -> Json { Json(InfoResponse { provider_id: state.provider_id.clone(), + provider_registration_info: state + .provider_info + .as_ref() + .and_then(|slot| slot.read().ok().map(|guard| guard.clone())), }) } @@ -748,6 +776,16 @@ async fn negotiate_terms( Error::Internal("provider node has no nonce counter; /negotiate disabled".to_string()) })?; + // Validate against the provider's on-chain settings *before* burning a + // nonce or spending CPU on a signature — the chain treats the signature + // as provider consent to whatever the client proposed. + let info = state + .provider_info + .as_ref() + .and_then(|slot| slot.read().ok().map(|guard| guard.clone())) + .ok_or(Error::ProviderInfoUnavailable)?; + negotiate::validate_request(&req, &info)?; + let terms: AgreementTermsOf = AgreementTerms { owner: req.owner, max_bytes: req.max_bytes, diff --git a/provider-node/src/command.rs b/provider-node/src/command.rs index 5dd39a07..871c9ea6 100644 --- a/provider-node/src/command.rs +++ b/provider-node/src/command.rs @@ -5,11 +5,11 @@ use crate::{ cli::{Cli, StorageMode, DEFAULT_PROVIDER_ID}, create_router, CheckpointCoordinator, CheckpointCoordinatorConfig, CheckpointCoordinatorHandle, DiskStorage, NonceCounter, ProviderState, ReplicaSyncCoordinator, ReplicaSyncCoordinatorConfig, - ReplicaSyncCoordinatorHandle, Storage, StorageBackend, + ReplicaSyncCoordinatorHandle, StateNonceCounter, StateProviderInfo, Storage, StorageBackend, }; use clap::Parser; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use std::time::Duration; use subxt::{dynamic::Value, OnlineClient, PolkadotConfig}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -64,6 +64,7 @@ pub async fn run() -> Result<(), Box> { } state.nonce_counter = Some(setup_nonce_counter(&cli, &state.provider_id).await?); + state.provider_info = Some(setup_provider_info(&cli, &state.provider_id).await?); Arc::new(state) } @@ -204,13 +205,52 @@ async fn start_replica_sync_coordinator( } } +/// Fetch the provider's on-chain registration info once and store it for +async fn setup_provider_info( + cli: &Cli, + provider_id: &str, +) -> Result> { + let provider_account = sp_runtime::AccountId32::from_str(provider_id) + .map_err(|e| format!("invalid provider SS58: {e:?}"))?; + + let mut client = storage_client::ProviderClient::new( + storage_client::ClientConfig { + chain_ws_url: cli.rpc.chain_rpc.clone(), + ..Default::default() + }, + provider_id.to_string(), + )?; + client.connect().await?; + + let info = client + .get_provider_info(&provider_account) + .await? + .ok_or_else(|| { + format!( + "provider {provider_id} is not registered on chain; \ + register it before starting the node" + ) + })?; + + tracing::info!( + "Loaded on-chain provider info: price_per_byte={}, duration=[{}, {}], max_capacity={}, accepting_primary={}", + info.price_per_byte, + info.min_duration, + info.max_duration, + info.max_capacity, + info.accepting_primary, + ); + + Ok(Arc::new(RwLock::new(info))) +} + /// Create the in-memory nonce counter and bootstrap it from the chain's /// `ProviderReplayState.hsn`. The chain is the source of truth, so there /// is nothing to persist locally. async fn setup_nonce_counter( cli: &Cli, provider_id: &str, -) -> Result, Box> { +) -> Result> { // Start the `nonce` from 1. let counter = NonceCounter::new(1); diff --git a/provider-node/src/error.rs b/provider-node/src/error.rs index 8dde76e2..15bbbdec 100644 --- a/provider-node/src/error.rs +++ b/provider-node/src/error.rs @@ -69,6 +69,34 @@ pub enum Error { #[error("Signing unavailable: provider has no keypair configured")] SigningUnavailable, + + #[error("Provider is not accepting new primary agreements")] + NotAcceptingPrimary, + + #[error("Provider is not accepting replica agreements")] + NotAcceptingReplicas, + + #[error("Proposed price_per_byte {proposed} is below the provider's listed price {listed}")] + PriceBelowListed { proposed: u128, listed: u128 }, + + #[error("Duration {duration} is outside the provider's bounds [{min}, {max}]")] + DurationOutOfBounds { duration: u32, min: u32, max: u32 }, + + #[error( + "Requested {requested} bytes exceeds remaining capacity \ + ({committed} of {max_capacity} bytes committed)" + )] + CapacityExceeded { + requested: u64, + committed: u64, + max_capacity: u64, + }, + + #[error("Provider on-chain info unavailable; cannot validate terms")] + ProviderInfoUnavailable, + + #[error("Too many requests")] + RateLimited, } #[derive(Serialize)] @@ -213,6 +241,74 @@ impl IntoResponse for Error { })), }, ), + Error::NotAcceptingPrimary => ( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorResponse { + error: "not_accepting_primary".to_string(), + details: None, + }, + ), + Error::NotAcceptingReplicas => ( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorResponse { + error: "not_accepting_replicas".to_string(), + details: None, + }, + ), + Error::PriceBelowListed { proposed, listed } => ( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorResponse { + error: "price_below_listed".to_string(), + // u128 doesn't fit serde_json numbers; send as strings. + details: Some(serde_json::json!({ + "proposed": proposed.to_string(), + "listed": listed.to_string(), + })), + }, + ), + Error::DurationOutOfBounds { duration, min, max } => ( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorResponse { + error: "duration_out_of_bounds".to_string(), + details: Some(serde_json::json!({ + "duration": duration, + "min": min, + "max": max, + })), + }, + ), + Error::CapacityExceeded { + requested, + committed, + max_capacity, + } => ( + StatusCode::UNPROCESSABLE_ENTITY, + ErrorResponse { + error: "capacity_exceeded".to_string(), + details: Some(serde_json::json!({ + "requested": requested, + "committed": committed, + "max_capacity": max_capacity, + })), + }, + ), + Error::ProviderInfoUnavailable => ( + StatusCode::SERVICE_UNAVAILABLE, + ErrorResponse { + error: "provider_info_unavailable".to_string(), + details: Some(serde_json::json!({ + "message": "provider's on-chain registration info is not loaded; \ + cannot validate agreement terms" + })), + }, + ), + Error::RateLimited => ( + StatusCode::TOO_MANY_REQUESTS, + ErrorResponse { + error: "rate_limited".to_string(), + details: None, + }, + ), }; (status, Json(error_response)).into_response() diff --git a/provider-node/src/lib.rs b/provider-node/src/lib.rs index 5e8f1f95..c309c7b5 100644 --- a/provider-node/src/lib.rs +++ b/provider-node/src/lib.rs @@ -51,10 +51,14 @@ pub use storage::{ pub use types::*; use sp_core::{crypto::Ss58Codec, sr25519, Pair}; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use std::time::Duration; +use storage_client::discovery::ProviderInfo; use tokio::sync::mpsc; +pub type StateNonceCounter = Arc; +pub type StateProviderInfo = Arc>; + /// Provider node state shared across handlers. pub struct ProviderState { /// Local storage backend @@ -77,7 +81,9 @@ pub struct ProviderState { pub auth_max_skew: Duration, /// Monotonic nonce counter used by `/negotiate` to allocate fresh /// nonces for provider-signed `AgreementTerms`. - pub nonce_counter: Option>, + pub nonce_counter: Option, + /// On-chain provider registration info. + pub provider_info: Option, } impl ProviderState { @@ -93,6 +99,7 @@ impl ProviderState { membership_cache: None, auth_max_skew: Duration::from_secs(300), nonce_counter: None, + provider_info: None, } } @@ -114,6 +121,7 @@ impl ProviderState { membership_cache: None, auth_max_skew: Duration::from_secs(300), nonce_counter: None, + provider_info: None, }) } diff --git a/provider-node/src/negotiate.rs b/provider-node/src/negotiate.rs index 34cd2a0e..ad29d707 100644 --- a/provider-node/src/negotiate.rs +++ b/provider-node/src/negotiate.rs @@ -14,14 +14,60 @@ //! 3. Signs `blake2_256(SCALE(terms))` with the provider's existing //! sr25519 checkpoint key (the same one used to sign commitments). +use crate::error::Error; use codec::Encode; use sp_core::Pair; use sp_runtime::MultiSignature; use std::sync::atomic::{AtomicU64, Ordering}; +use storage_client::discovery::ProviderInfo; // Wire types are shared with the SDK so client + server agree on serde shape. pub use storage_client::agreement::{AgreementTermsOf, NegotiateRequest, SignedTerms}; +/// Validate a negotiation request against the provider's current on-chain +/// settings. +/// +/// The chain treats the resulting signature as provider consent, so the +/// node must refuse to sign terms it wouldn't accept: without this check a +/// client could propose `price_per_byte = 0`, an out-of-range duration, or +/// more bytes than the provider has capacity for, and the extrinsic would +/// bind the provider to it. +pub fn validate_request(req: &NegotiateRequest, info: &ProviderInfo) -> Result<(), Error> { + match &req.replica_params { + None if !info.accepting_primary => return Err(Error::NotAcceptingPrimary), + Some(_) if info.replica_sync_price.is_none() => return Err(Error::NotAcceptingReplicas), + _ => {} + } + + if req.price_per_byte < info.price_per_byte { + return Err(Error::PriceBelowListed { + proposed: req.price_per_byte, + listed: info.price_per_byte, + }); + } + + if req.duration < info.min_duration || req.duration > info.max_duration { + return Err(Error::DurationOutOfBounds { + duration: req.duration, + min: info.min_duration, + max: info.max_duration, + }); + } + + // `max_capacity == 0` means unlimited. + if info.max_capacity > 0 + && info.committed_bytes.saturating_add(req.max_bytes) > info.max_capacity + { + return Err(Error::CapacityExceeded { + requested: req.max_bytes, + committed: info.committed_bytes, + max_capacity: info.max_capacity, + }); + } + + Ok(()) +} + /// In-memory monotonic nonce counter for provider-signed terms. /// /// Nonces are atomically allocated via [`Self::next`]. There is no local @@ -93,6 +139,138 @@ pub fn sign_terms(keypair: &sp_core::sr25519::Pair, terms: &AgreementTermsOf) -> #[cfg(test)] mod tests { use super::*; + use sp_runtime::AccountId32; + use storage_client::agreement::ReplicaTermsOf; + + fn provider_info() -> ProviderInfo { + ProviderInfo { + multiaddr: "/ip4/127.0.0.1/tcp/3333".to_string(), + stake: 1_000_000_000_000, + committed_bytes: 0, + max_capacity: 0, + min_duration: 10, + max_duration: 100_000, + price_per_byte: 5, + accepting_primary: true, + replica_sync_price: None, + accepting_extensions: true, + agreements_total: 0, + challenges_failed: 0, + } + } + + fn request() -> NegotiateRequest { + NegotiateRequest { + owner: AccountId32::new([0u8; 32]), + max_bytes: 1024, + duration: 50, + price_per_byte: 5, + bucket_id: None, + replica_params: None, + } + } + + #[test] + fn accepts_request_matching_settings() { + assert!(validate_request(&request(), &provider_info()).is_ok()); + } + + #[test] + fn rejects_price_below_listed() { + let mut req = request(); + req.price_per_byte = 0; + let err = validate_request(&req, &provider_info()).unwrap_err(); + assert!(matches!( + err, + Error::PriceBelowListed { + proposed: 0, + listed: 5 + } + )); + } + + #[test] + fn accepts_price_above_listed() { + let mut req = request(); + req.price_per_byte = 10; + assert!(validate_request(&req, &provider_info()).is_ok()); + } + + #[test] + fn rejects_duration_out_of_bounds() { + let mut req = request(); + req.duration = 5; + assert!(matches!( + validate_request(&req, &provider_info()), + Err(Error::DurationOutOfBounds { .. }) + )); + req.duration = 100_001; + assert!(matches!( + validate_request(&req, &provider_info()), + Err(Error::DurationOutOfBounds { .. }) + )); + } + + #[test] + fn rejects_bytes_beyond_remaining_capacity() { + let mut info = provider_info(); + info.max_capacity = 2048; + info.committed_bytes = 1536; + let err = validate_request(&request(), &info).unwrap_err(); + assert!(matches!( + err, + Error::CapacityExceeded { + requested: 1024, + committed: 1536, + max_capacity: 2048 + } + )); + } + + #[test] + fn zero_capacity_means_unlimited() { + let mut req = request(); + req.max_bytes = u64::MAX; + assert!(validate_request(&req, &provider_info()).is_ok()); + } + + #[test] + fn rejects_primary_when_not_accepting() { + let mut info = provider_info(); + info.accepting_primary = false; + assert!(matches!( + validate_request(&request(), &info), + Err(Error::NotAcceptingPrimary) + )); + } + + #[test] + fn rejects_replica_without_sync_price() { + let mut req = request(); + req.bucket_id = Some(1); + req.replica_params = Some(ReplicaTermsOf { + sync_balance: 1_000, + min_sync_interval: 10, + }); + assert!(matches!( + validate_request(&req, &provider_info()), + Err(Error::NotAcceptingReplicas) + )); + } + + #[test] + fn accepts_replica_even_when_primary_closed() { + let mut info = provider_info(); + info.accepting_primary = false; + info.replica_sync_price = Some(7); + let mut req = request(); + req.bucket_id = Some(1); + req.replica_params = Some(ReplicaTermsOf { + sync_balance: 1_000, + min_sync_interval: 10, + }); + assert!(validate_request(&req, &info).is_ok()); + } #[test] fn nonce_counter_is_monotonic() { diff --git a/provider-node/src/types.rs b/provider-node/src/types.rs index c69c2219..b16e8d9a 100644 --- a/provider-node/src/types.rs +++ b/provider-node/src/types.rs @@ -1,6 +1,7 @@ //! API types for the provider node. use serde::{Deserialize, Serialize}; +use storage_client::discovery::ProviderInfo; use storage_primitives::BucketId; // ───────────────────────────────────────────────────────────────────────────── @@ -246,7 +247,7 @@ pub struct ListBucketsResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InfoResponse { pub provider_id: String, - // TODO: Add more provider information + pub provider_registration_info: Option, } /// Health check response. From a198c5b6bdbe0afe52771b2605cb33e8921aec84 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:04:44 +0700 Subject: [PATCH 35/44] fix: return 503 with defined errors when /negotiate prerequisites are missing - /negotiate now returns SigningUnavailable (503) instead of a generic 500 when the node has no keypair or nonce counter - make nonce counter and provider info optional at startup instead of failing or silently starting from a default nonce - initialize/remove ProviderReplayStates on provider (de)registration --- pallet/src/lib.rs | 2 ++ provider-node/src/api.rs | 19 +++++++++---------- provider-node/src/command.rs | 28 ++++++++++------------------ provider-node/src/lib.rs | 8 ++++---- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index 4a0700e9..b32f230a 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -877,6 +877,7 @@ pub mod pallet { }; Providers::::insert(&who, provider_info); + ProviderReplayStates::::insert(&who, ReplayWindow::default()); Self::deposit_event(Event::ProviderRegistered { provider: who, @@ -1014,6 +1015,7 @@ pub mod pallet { T::Currency::unreserve(&who, provider.stake); Providers::::remove(&who); + ProviderReplayStates::::remove(&who); Self::deposit_event(Event::ProviderDeregistered { provider: who, diff --git a/provider-node/src/api.rs b/provider-node/src/api.rs index 3769c87b..cb988000 100644 --- a/provider-node/src/api.rs +++ b/provider-node/src/api.rs @@ -764,21 +764,20 @@ async fn get_historical_roots( /// /// TODO: Request are automatically accepted, implement advance features to let providers determine /// -/// Returns `503` if the node has no signing key (no `--keyfile`) or no +/// Returns `503` if +/// - The node has no signing key (no `--keyfile`) or no +/// - The node fails to fetch provider registration info async fn negotiate_terms( State(state): State>, Json(req): Json, ) -> Result, Error> { - let keypair = state.keypair.as_ref().ok_or_else(|| { - Error::Internal("provider node has no signing key; /negotiate disabled".to_string()) - })?; - let nonce_counter = state.nonce_counter.as_ref().ok_or_else(|| { - Error::Internal("provider node has no nonce counter; /negotiate disabled".to_string()) - })?; + let keypair = state.keypair.as_ref().ok_or(Error::SigningUnavailable)?; + let nonce_counter = state + .nonce_counter + .as_ref() + .ok_or(Error::SigningUnavailable)?; - // Validate against the provider's on-chain settings *before* burning a - // nonce or spending CPU on a signature — the chain treats the signature - // as provider consent to whatever the client proposed. + // Validate against the provider's on-chain settings let info = state .provider_info .as_ref() diff --git a/provider-node/src/command.rs b/provider-node/src/command.rs index 871c9ea6..27a74cc0 100644 --- a/provider-node/src/command.rs +++ b/provider-node/src/command.rs @@ -63,8 +63,8 @@ pub async fn run() -> Result<(), Box> { ); } - state.nonce_counter = Some(setup_nonce_counter(&cli, &state.provider_id).await?); - state.provider_info = Some(setup_provider_info(&cli, &state.provider_id).await?); + state.nonce_counter = setup_nonce_counter(&cli, &state.provider_id).await?; + state.provider_info = setup_provider_info(&cli, &state.provider_id).await?; Arc::new(state) } @@ -241,7 +241,7 @@ async fn setup_provider_info( info.accepting_primary, ); - Ok(Arc::new(RwLock::new(info))) + Ok(Some(Arc::new(RwLock::new(info)))) } /// Create the in-memory nonce counter and bootstrap it from the chain's @@ -251,12 +251,8 @@ async fn setup_nonce_counter( cli: &Cli, provider_id: &str, ) -> Result> { - // Start the `nonce` from 1. - let counter = NonceCounter::new(1); - // Bootstrap from on-chain hsn. Best-effort: if the chain isn't - // reachable yet, start from 0 — the on-chain replay window will - // reject any out-of-range reissues anyway. + // reachable yet, set to None let provider_account = sp_runtime::AccountId32::from_str(provider_id) .map_err(|e| format!("invalid provider SS58: {e:?}"))?; match storage_client::ProviderClient::fetch_replay_hsn(&cli.rpc.chain_rpc, &provider_account) @@ -268,23 +264,19 @@ async fn setup_nonce_counter( hsn, provider_id, ); + let counter = NonceCounter::new(1); counter.bootstrap_from_hsn(hsn); + Ok(Some(Arc::new(counter))) } Ok(None) => { - tracing::info!( - "No on-chain replay state for provider {} yet; starting nonce counter from 0", - provider_id, - ); + tracing::warn!("No on-chain replay state for provider {} yet.", provider_id,); + Ok(None) } Err(e) => { - tracing::warn!( - "Failed to bootstrap nonce counter from chain: {}; starting nonce counter from 0", - e, - ); + tracing::warn!("Failed to bootstrap nonce counter from chain: {}.", e,); + Ok(None) } } - - Ok(Arc::new(counter)) } /// Convert a bind address (e.g. "0.0.0.0:3333") to a multiaddr string (e.g. "/ip4/127.0.0.1/tcp/3333"). diff --git a/provider-node/src/lib.rs b/provider-node/src/lib.rs index c309c7b5..d0eb53f2 100644 --- a/provider-node/src/lib.rs +++ b/provider-node/src/lib.rs @@ -56,8 +56,8 @@ use std::time::Duration; use storage_client::discovery::ProviderInfo; use tokio::sync::mpsc; -pub type StateNonceCounter = Arc; -pub type StateProviderInfo = Arc>; +pub type StateNonceCounter = Option>; +pub type StateProviderInfo = Option>>; /// Provider node state shared across handlers. pub struct ProviderState { @@ -81,9 +81,9 @@ pub struct ProviderState { pub auth_max_skew: Duration, /// Monotonic nonce counter used by `/negotiate` to allocate fresh /// nonces for provider-signed `AgreementTerms`. - pub nonce_counter: Option, + pub nonce_counter: StateNonceCounter, /// On-chain provider registration info. - pub provider_info: Option, + pub provider_info: StateProviderInfo, } impl ProviderState { From bd659ef17403198b9dd482c9d626c6f079908adf Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:13:04 +0700 Subject: [PATCH 36/44] feat: domain-separate primary and replica term signatures The signed payload is now blake2_256(TERM_CONTEXT | SCALE(terms)), with PRIMARY_TERM_CONTEXT = 'primary-term-v1:' and REPLICA_TERM_CONTEXT = 'replica-term-v1:'. The verifier takes the context from the redemption path rather than the terms themselves, so a quote signed for one flavour can never be redeemed as the other. --- client/src/agreement.rs | 27 +++++++------- client/src/substrate.rs | 4 +-- pallet/src/benchmarking.rs | 2 +- pallet/src/lib.rs | 32 +++++++++++++---- pallet/src/tests.rs | 3 +- primitives/src/agreement_term.rs | 35 +++++++++++++++++-- provider-node/src/negotiate.rs | 13 +++---- runtimes/web3-storage-paseo/tests/tests.rs | 2 +- .../file-system/pallet-registry/src/tests.rs | 3 +- .../s3/pallet-s3-registry/src/tests.rs | 3 +- 10 files changed, 88 insertions(+), 36 deletions(-) diff --git a/client/src/agreement.rs b/client/src/agreement.rs index 381d43d3..904c6129 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -10,11 +10,12 @@ //! that need to sign terms without going through the full provider //! keystore. //! -//! The on-chain pallet hashes `blake2_256(SCALE(terms))` and verifies the -//! signature against the provider's registered public key, so the same -//! encoding has to be used on both sides — `sign_terms` enforces that. +//! The on-chain pallet hashes `blake2_256(TERM_CONTEXT | SCALE(terms))` — +//! `primary-term-v1:` or `replica-term-v1:` depending on the redemption +//! path — and verifies the signature against the provider's registered +//! public key, so the same payload has to be built on both sides — +//! `sign_terms` enforces that via [`AgreementTerms::signing_payload`]. -use codec::Encode; use serde::{Deserialize, Deserializer, Serialize}; use sp_core::hashing::blake2_256; use sp_runtime::{AccountId32, MultiSignature}; @@ -60,10 +61,11 @@ pub struct NegotiateRequest { /// Provider-signed agreement terms /// -/// `signature` is a `MultiSignature` over `blake2_256(SCALE(terms))`, -/// produced by the provider's registered key. We carry the signature as -/// hex over the wire — `MultiSignature` doesn't derive serde directly, -/// and hex keeps the JSON readable. +/// `signature` is a `MultiSignature` over +/// `blake2_256(TERM_CONTEXT | SCALE(terms))`, produced by the provider's +/// registered key. We carry the signature as hex over the wire — +/// `MultiSignature` doesn't derive serde directly, and hex keeps the JSON +/// readable. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignedTerms { pub terms: AgreementTermsOf, @@ -73,14 +75,15 @@ pub struct SignedTerms { /// Sign already-built terms with a provider keypair. /// -/// Mirror of the on-chain `verify_terms_signature`: SCALE-encode, hash -/// with blake2-256, then sign. The runtime accepts `MultiSignature`, so -/// callers wrap the raw sr25519 signature with `MultiSignature::Sr25519`. +/// Mirror of the on-chain `verify_terms_signature`: prefix the matching +/// term context, SCALE-encode, hash with blake2-256, then sign. The +/// runtime accepts `MultiSignature`, so callers wrap the raw sr25519 +/// signature with `MultiSignature::Sr25519`. pub fn sign_terms( keypair: &subxt_signer::sr25519::Keypair, terms: &AgreementTermsOf, ) -> MultiSignature { - let hash = blake2_256(&terms.encode()); + let hash = blake2_256(&terms.signing_payload()); let raw = keypair.sign(&hash); MultiSignature::Sr25519(sp_core::sr25519::Signature::from_raw(raw.0)) } diff --git a/client/src/substrate.rs b/client/src/substrate.rs index 221690f8..7d1fda5e 100644 --- a/client/src/substrate.rs +++ b/client/src/substrate.rs @@ -247,8 +247,8 @@ pub mod extrinsics { /// /// Bundles the SCALE-encoded provider-signed terms and signature into /// the dynamic call shape Layer 0 expects. The chain hashes - /// `blake2_256(SCALE(terms))` and verifies the signature against the - /// provider's registered public key. + /// `blake2_256(TERM_CONTEXT | SCALE(terms))` and verifies the + /// signature against the provider's registered public key. pub fn establish_storage_agreement( provider: AccountId32, terms: &crate::agreement::AgreementTermsOf, diff --git a/pallet/src/benchmarking.rs b/pallet/src/benchmarking.rs index 0774a207..0fb61de4 100644 --- a/pallet/src/benchmarking.rs +++ b/pallet/src/benchmarking.rs @@ -107,7 +107,7 @@ fn sign_terms( public_key: &sp_core::sr25519::Public, terms: &AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&codec::Encode::encode(terms)); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); let sig = sp_io::crypto::sr25519_sign(KEY_TYPE, public_key, &hash) .expect("benchmarking keystore signs with a key it generated"); sp_runtime::MultiSignature::Sr25519(sig) diff --git a/pallet/src/lib.rs b/pallet/src/lib.rs index b32f230a..f3a20a63 100644 --- a/pallet/src/lib.rs +++ b/pallet/src/lib.rs @@ -2774,11 +2774,17 @@ pub mod pallet { /// Verify a provider signature over a SCALE-encoded /// [`AgreementTermsOf`]. The signed payload is - /// `blake2_256(terms.encode())`. + /// `blake2_256(context | terms.encode())`, where `context` is the + /// domain-separation prefix for the redemption path + /// ([`storage_primitives::PRIMARY_TERM_CONTEXT`] or + /// [`storage_primitives::REPLICA_TERM_CONTEXT`]) — the caller, not + /// the terms, decides it, so a quote signed for one flavour can + /// never be redeemed as the other. fn verify_terms_signature( provider_info: &ProviderInfo, terms: &AgreementTermsOf, sig: &sp_runtime::MultiSignature, + context: &[u8], ) -> DispatchResult { use sp_runtime::traits::Verify; @@ -2797,7 +2803,9 @@ pub mod pallet { } }; - let hash = sp_io::hashing::blake2_256(&terms.encode()); + let mut payload = context.to_vec(); + terms.encode_to(&mut payload); + let hash = sp_io::hashing::blake2_256(&payload); ensure!( sig.verify(&hash[..], &account_id), Error::::InvalidProviderSignature @@ -3772,10 +3780,16 @@ pub mod pallet { let current_block = frame_system::Pallet::::block_number(); ensure!(terms.valid_until >= current_block, Error::::TermsExpired); - // Provider lookup + signature check over blake2_256(SCALE(terms)). + // Provider lookup + signature check over + // blake2_256(PRIMARY_TERM_CONTEXT | SCALE(terms)). let provider_info = Providers::::get(provider).ok_or(Error::::ProviderNotFound)?; - Self::verify_terms_signature(&provider_info, &terms, sig)?; + Self::verify_terms_signature( + &provider_info, + &terms, + sig, + storage_primitives::PRIMARY_TERM_CONTEXT, + )?; // Replay window: at most once per nonce, within the trailing 256 slots. ProviderReplayStates::::try_mutate(provider, |window| -> DispatchResult { @@ -3905,10 +3919,16 @@ pub mod pallet { .ok_or(Error::::MissingReplicaTerms)? .clone(); - // Provider lookup + signature check over blake2_256(SCALE(terms)). + // Provider lookup + signature check over + // blake2_256(REPLICA_TERM_CONTEXT | SCALE(terms)). let provider_info = Providers::::get(provider).ok_or(Error::::ProviderNotFound)?; - Self::verify_terms_signature(&provider_info, &terms, sig)?; + Self::verify_terms_signature( + &provider_info, + &terms, + sig, + storage_primitives::REPLICA_TERM_CONTEXT, + )?; // Replay window: at most once per nonce, within the trailing 256 slots. ProviderReplayStates::::try_mutate(provider, |window| -> DispatchResult { diff --git a/pallet/src/tests.rs b/pallet/src/tests.rs index 58543166..53e44e3f 100644 --- a/pallet/src/tests.rs +++ b/pallet/src/tests.rs @@ -1,7 +1,6 @@ //! Tests for the storage provider pallet. use crate::{mock::*, *}; -use codec::Encode; use frame_support::{assert_noop, assert_ok}; use sp_core::crypto::KeyTypeId; use storage_primitives::{ @@ -38,7 +37,7 @@ fn sign_terms( public: &sp_core::sr25519::Public, terms: &AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&terms.encode()); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); let sig = sp_io::crypto::sr25519_sign(PROVIDER_KEY_TYPE, public, &hash) .expect("keystore should sign with a key it generated"); sp_runtime::MultiSignature::Sr25519(sig) diff --git a/primitives/src/agreement_term.rs b/primitives/src/agreement_term.rs index d82b0f6a..35fbd5aa 100644 --- a/primitives/src/agreement_term.rs +++ b/primitives/src/agreement_term.rs @@ -1,16 +1,24 @@ //! Provider-signed terms of a storage agreement. //! -//! A provider quotes terms off-chain (e.g. over HTTP) and signs the SCALE -//! encoding of an `AgreementTerms` value. +//! A provider quotes terms off-chain (e.g. over HTTP) and signs +//! `blake2_256(TERM_CONTEXT | SCALE(terms))`, where the context string +//! domain-separates primary quotes from replica quotes. //! //! [`AgreementTerms`] shape covers both flavours: `replica` is //! `None` for primary agreements and `Some(_)` for replica agreements, //! carrying the per-sync funding parameters. +use alloc::vec::Vec; use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use core::fmt::Debug; use scale_info::TypeInfo; +/// Domain-separation context prefixed to the signing payload of primary terms. +pub const PRIMARY_TERM_CONTEXT: &[u8] = b"primary-term-v1:"; + +/// Domain-separation context prefixed to the signing payload of replica terms. +pub const REPLICA_TERM_CONTEXT: &[u8] = b"replica-term-v1:"; + /// Off-chain quote signed by the provider and redeemed on-chain by the owner. #[derive( Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, @@ -42,6 +50,29 @@ pub struct AgreementTerms { pub replica_params: Option>, } +impl + AgreementTerms +{ + /// Domain-separation context matching this quote's flavour: + /// [`REPLICA_TERM_CONTEXT`] when `replica_params` is `Some(_)`, + /// [`PRIMARY_TERM_CONTEXT`] otherwise. + pub fn signing_context(&self) -> &'static [u8] { + if self.replica_params.is_some() { + REPLICA_TERM_CONTEXT + } else { + PRIMARY_TERM_CONTEXT + } + } + + /// Bytes the provider hashes (blake2-256) and signs: + /// `signing_context() | SCALE(self)`. + pub fn signing_payload(&self) -> Vec { + let mut payload = self.signing_context().to_vec(); + self.encode_to(&mut payload); + payload + } +} + /// Replica terms #[derive( Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, Debug, diff --git a/provider-node/src/negotiate.rs b/provider-node/src/negotiate.rs index ad29d707..eb52dc64 100644 --- a/provider-node/src/negotiate.rs +++ b/provider-node/src/negotiate.rs @@ -11,11 +11,12 @@ //! 2. Builds [`AgreementTerms`] from the request, the provider's current //! `price_per_byte` setting (read from chain), and //! `valid_until = current_block + valid_until_offset`. -//! 3. Signs `blake2_256(SCALE(terms))` with the provider's existing -//! sr25519 checkpoint key (the same one used to sign commitments). +//! 3. Signs `blake2_256(TERM_CONTEXT | SCALE(terms))` with the provider's +//! existing sr25519 checkpoint key (the same one used to sign +//! commitments). The context is `primary-term-v1:` or +//! `replica-term-v1:` depending on the quote's flavour. use crate::error::Error; -use codec::Encode; use sp_core::Pair; use sp_runtime::MultiSignature; use std::sync::atomic::{AtomicU64, Ordering}; @@ -128,10 +129,10 @@ impl NonceCounter { /// Sign agreement terms with the provider's checkpoint sr25519 key. /// -/// Mirrors the on-chain verifier: SCALE-encode → blake2-256 → sr25519 -/// sign → wrap as `MultiSignature::Sr25519`. +/// Mirrors the on-chain verifier: term context | SCALE-encode → +/// blake2-256 → sr25519 sign → wrap as `MultiSignature::Sr25519`. pub fn sign_terms(keypair: &sp_core::sr25519::Pair, terms: &AgreementTermsOf) -> MultiSignature { - let hash = sp_core::hashing::blake2_256(&terms.encode()); + let hash = sp_core::hashing::blake2_256(&terms.signing_payload()); let sig = keypair.sign(&hash); MultiSignature::Sr25519(sig) } diff --git a/runtimes/web3-storage-paseo/tests/tests.rs b/runtimes/web3-storage-paseo/tests/tests.rs index 738332d5..29a6e2eb 100644 --- a/runtimes/web3-storage-paseo/tests/tests.rs +++ b/runtimes/web3-storage-paseo/tests/tests.rs @@ -168,7 +168,7 @@ fn sign_primary_terms( provider: Sr25519Keyring, terms: &pallet_storage_provider::AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&terms.encode()); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); sp_runtime::MultiSignature::Sr25519(provider.pair().sign(&hash)) } diff --git a/storage-interfaces/file-system/pallet-registry/src/tests.rs b/storage-interfaces/file-system/pallet-registry/src/tests.rs index 864716f5..71cf1740 100644 --- a/storage-interfaces/file-system/pallet-registry/src/tests.rs +++ b/storage-interfaces/file-system/pallet-registry/src/tests.rs @@ -2,7 +2,6 @@ use crate::{ mock::{MaxMultiaddrLength, *}, Drives, Error, }; -use codec::Encode; use frame_support::{assert_noop, assert_ok, traits::ConstU32, BoundedVec}; use pallet_storage_provider::{AgreementTermsOf, ProviderSettings}; use sp_core::crypto::KeyTypeId; @@ -24,7 +23,7 @@ fn sign_terms( public: &sp_core::sr25519::Public, terms: &AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&terms.encode()); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); let sig = sp_io::crypto::sr25519_sign(PROVIDER_KEY_TYPE, public, &hash) .expect("keystore signs with a key it generated"); sp_runtime::MultiSignature::Sr25519(sig) diff --git a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs index e8808cdf..2ff469a5 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs @@ -1,7 +1,6 @@ //! Tests for S3 Registry pallet. use crate::{mock::*, Error, S3Buckets}; -use codec::Encode; use frame_support::{assert_noop, assert_ok, traits::ConstU32, BoundedVec}; use pallet_storage_provider::{AgreementTermsOf, ProviderSettings}; use sp_core::crypto::KeyTypeId; @@ -28,7 +27,7 @@ fn sign_terms( public: &sp_core::sr25519::Public, terms: &AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&terms.encode()); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); let sig = sp_io::crypto::sr25519_sign(PROVIDER_KEY_TYPE, public, &hash) .expect("keystore signs with a key it generated"); sp_runtime::MultiSignature::Sr25519(sig) From 17528eb09353be243b66d4a5bb4368fdce8749be Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:36:18 +0700 Subject: [PATCH 37/44] fix: send bigint terms fields as strings in /negotiate requests Pass max_bytes and price_per_byte as BigInt in all negotiateTerms call sites, matching the PAPI descriptor types. Raw JSON numbers fail on the provider's u128 fields because serde's untagged enum buffers through a Content type that cannot represent u128; the existing JSON.stringify replacer serializes BigInt as strings, which parse via FromStr. --- examples/papi/bucket-membership.js | 4 ++-- examples/papi/bucket-with-storage.js | 4 ++-- examples/papi/checkpoint-missed.js | 4 ++-- examples/papi/checkpoint-rewards.js | 4 ++-- examples/papi/drive-lifecycle.js | 4 ++-- examples/papi/full-flow.js | 4 ++-- examples/papi/sc-api.js | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/papi/bucket-membership.js b/examples/papi/bucket-membership.js index eaf2f8d1..5e47442b 100644 --- a/examples/papi/bucket-membership.js +++ b/examples/papi/bucket-membership.js @@ -82,9 +82,9 @@ async function main() { console.log("\n=== Step 2: Negotiate signed agreement terms ==="); const signed = await negotiateTerms(PROVIDER_URL, { owner: admin.address, - max_bytes: 1_048_576, // 1 MiB + max_bytes: 1_048_576n, // 1 MiB duration: 100, - price_per_byte: 1, + price_per_byte: 1n, replica_params: null, }); console.log( diff --git a/examples/papi/bucket-with-storage.js b/examples/papi/bucket-with-storage.js index ae49c013..4cefa071 100644 --- a/examples/papi/bucket-with-storage.js +++ b/examples/papi/bucket-with-storage.js @@ -69,9 +69,9 @@ async function main() { console.log("\n=== Step 2: Negotiate signed agreement terms ==="); const signed = await negotiateTerms(PROVIDER_URL, { owner: client.address, - max_bytes: 1_048_576, // 1 MiB + max_bytes: 1_048_576n, // 1 MiB duration: 50, - price_per_byte: 0, + price_per_byte: 0n, replica_params: null, }); console.log( diff --git a/examples/papi/checkpoint-missed.js b/examples/papi/checkpoint-missed.js index b927bfbd..e88a11b1 100644 --- a/examples/papi/checkpoint-missed.js +++ b/examples/papi/checkpoint-missed.js @@ -74,9 +74,9 @@ async function main() { const signed = await negotiateTerms(PROVIDER_URL, { owner: client.address, - max_bytes: 1_048_576, // 1 MiB + max_bytes: 1_048_576n, // 1 MiB duration: 200, - price_per_byte: 1, + price_per_byte: 1n, replica_params: null, }); const bucketId = await establishStorageAgreement(api, client, provider, signed); diff --git a/examples/papi/checkpoint-rewards.js b/examples/papi/checkpoint-rewards.js index 19c933e8..4e9d02f6 100644 --- a/examples/papi/checkpoint-rewards.js +++ b/examples/papi/checkpoint-rewards.js @@ -177,9 +177,9 @@ async function main() { console.log("\n=== Step 2: Negotiate + establish_storage_agreement (atomic) ==="); const signed = await negotiateTerms(PROVIDER_URL, { owner: client.address, - max_bytes: 1_048_576, // 1 MiB + max_bytes: 1_048_576n, // 1 MiB duration: 200, - price_per_byte: 1, + price_per_byte: 1n, replica_params: null, }); const bucketId = await establishStorageAgreement(api, client, provider, signed); diff --git a/examples/papi/drive-lifecycle.js b/examples/papi/drive-lifecycle.js index 1f525a1a..9d771ae1 100644 --- a/examples/papi/drive-lifecycle.js +++ b/examples/papi/drive-lifecycle.js @@ -99,13 +99,13 @@ async function main() { await ensureProviderRegistered(api, provider, PROVIDER_URL); console.log("\n=== Step 2: Negotiate signed agreement terms ==="); - const maxBytes = 1_048_576; // 1 MiB + const maxBytes = 1_048_576n; // 1 MiB const duration = 200; const signed = await negotiateTerms(PROVIDER_URL, { owner: owner.address, max_bytes: maxBytes, duration, - price_per_byte: 1, + price_per_byte: 1n, replica_params: null, }); console.log( diff --git a/examples/papi/full-flow.js b/examples/papi/full-flow.js index f6339430..4094f570 100644 --- a/examples/papi/full-flow.js +++ b/examples/papi/full-flow.js @@ -63,9 +63,9 @@ async function setupAgreement(api, providerUrl, client, provider) { ); const signed = await negotiateTerms(providerUrl, { owner: client.address, - max_bytes: Number(maxBytes), + max_bytes: maxBytes, duration, - price_per_byte: 1, + price_per_byte: 1n, replica_params: null, }); console.log( diff --git a/examples/papi/sc-api.js b/examples/papi/sc-api.js index 3e8fd25e..4ebfd688 100644 --- a/examples/papi/sc-api.js +++ b/examples/papi/sc-api.js @@ -52,9 +52,9 @@ export function h160ToSubstrate(addressBytes) { export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, duration }) { const signed = await negotiateTerms(providerUrl, { owner: owner.address, - max_bytes: Number(maxBytes), + max_bytes: BigInt(maxBytes), duration, - price_per_byte: 0, + price_per_byte: 0n, replica_params: null, bucket_id: null, }); From 1feb03302287e7a9efb76f21566f6703c7bc3cbf Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:18:50 +0700 Subject: [PATCH 38/44] ci: register providers on-chain before starting provider nodes The provider node now requires its account to be registered on chain at startup (setup_provider_info fails hard otherwise), so the demos can no longer be the ones to register Alice/Charlie after the nodes are up. Register both providers via the register_provider example right after the parachain produces blocks, and give each provider its own log file so the disk node no longer clobbers the inmemory node's log. --- .github/workflows/integration-tests.yml | 26 ++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0dd7a191..a9617593 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -116,13 +116,21 @@ jobs: - name: Wait for parachain blocks uses: ./.github/actions/wait-for-parachain - - name: Start provider (inmemory) and wait for health + - name: Register providers on-chain run: | echo "//Alice" > /tmp/alice-key && chmod 600 /tmp/alice-key + echo "//Charlie" > /tmp/charlie-key && chmod 600 /tmp/charlie-key + cargo run --release -p storage-client --example register_provider \ + ws://127.0.0.1:2222 http://127.0.0.1:3333 /ip4/127.0.0.1/tcp/3333 /tmp/alice-key + cargo run --release -p storage-client --example register_provider \ + ws://127.0.0.1:2222 http://127.0.0.1:3334 /ip4/127.0.0.1/tcp/3334 /tmp/charlie-key + + - name: Start provider (inmemory) and wait for health + run: | nohup ./target/release/storage-provider-node \ --keyfile /tmp/alice-key --storage-mode inmemory \ --bind-addr 0.0.0.0:3333 --chain-rpc ws://127.0.0.1:2222 \ - --enable-checkpoint-coordinator > /tmp/provider.log 2>&1 & + --enable-checkpoint-coordinator > /tmp/provider-inmemory.log 2>&1 & echo "Waiting for provider HTTP server..." for i in $(seq 1 60); do if curl -s http://127.0.0.1:3333/health | jq -e '.status' > /dev/null 2>&1; then @@ -131,7 +139,7 @@ jobs: fi if [ "$i" -eq 60 ]; then echo "Timeout: provider did not become healthy" - cat /tmp/provider.log || true + cat /tmp/provider-inmemory.log || true exit 1 fi sleep 2 @@ -139,11 +147,10 @@ jobs: - name: Start provider (disk) and wait for health run: | - echo "//Charlie" > /tmp/charlie-key && chmod 600 /tmp/charlie-key nohup ./target/release/storage-provider-node \ --keyfile /tmp/charlie-key --storage-mode disk --storage-path /tmp/provider-data \ --bind-addr 0.0.0.0:3334 --chain-rpc ws://127.0.0.1:2222 \ - --enable-checkpoint-coordinator > /tmp/provider.log 2>&1 & + --enable-checkpoint-coordinator > /tmp/provider-disk.log 2>&1 & echo "Waiting for disk provider HTTP server..." for i in $(seq 1 60); do if curl -s http://127.0.0.1:3334/health | jq -e '.status' > /dev/null 2>&1; then @@ -190,7 +197,7 @@ jobs: name: integration-test-logs-${{ matrix.runtime.name }} path: | /tmp/zombienet.log - /tmp/provider.log + /tmp/provider-inmemory.log /tmp/provider-disk.log /tmp/zombie-*/*.log /tmp/zombie-*/**/*.log @@ -305,9 +312,14 @@ jobs: - name: Wait for parachain blocks uses: ./.github/actions/wait-for-parachain - - name: Start provider (inmemory) and wait for health + - name: Register provider on-chain run: | echo "//Alice" > /tmp/alice-key && chmod 600 /tmp/alice-key + cargo run --release -p storage-client --example register_provider \ + ws://127.0.0.1:2222 http://127.0.0.1:3333 /ip4/127.0.0.1/tcp/3333 /tmp/alice-key + + - name: Start provider (inmemory) and wait for health + run: | nohup ./target/release/storage-provider-node \ --keyfile /tmp/alice-key --storage-mode inmemory \ --bind-addr 0.0.0.0:3333 --chain-rpc ws://127.0.0.1:2222 \ From 2fc19cd41fb1b722a5176768e09c8965a22202ec Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:34:23 +0700 Subject: [PATCH 39/44] feat: update storage interface benchmarks --- .../file-system/pallet-registry/src/bechmarking.rs | 2 +- storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs b/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs index e3521e30..5cb0ff87 100644 --- a/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs +++ b/storage-interfaces/file-system/pallet-registry/src/bechmarking.rs @@ -92,7 +92,7 @@ fn sign_terms( public_key: &sp_core::sr25519::Public, terms: &AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&codec::Encode::encode(terms)); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); let sig = sp_io::crypto::sr25519_sign(KEY_TYPE, public_key, &hash) .expect("benchmarking keystore signs with a key it generated"); sp_runtime::MultiSignature::Sr25519(sig) diff --git a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs index 2642c98f..101a5447 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/benchmarking.rs @@ -97,7 +97,7 @@ fn sign_terms( public_key: &sp_core::sr25519::Public, terms: &AgreementTermsOf, ) -> sp_runtime::MultiSignature { - let hash = sp_io::hashing::blake2_256(&codec::Encode::encode(terms)); + let hash = sp_io::hashing::blake2_256(&terms.signing_payload()); let sig = sp_io::crypto::sr25519_sign(KEY_TYPE, public_key, &hash) .expect("benchmarking keystore signs with a key it generated"); sp_runtime::MultiSignature::Sr25519(sig) From 972d9c49c8955f33e43e1da23829a8f8dd95b69f Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:42:34 +0700 Subject: [PATCH 40/44] fix: examples smart contract ensure price_per_byte greater than provider quote --- examples/papi/sc-api.js | 4 ++-- examples/papi/sc-coverage.js | 11 ++++++----- examples/papi/sc-flow.js | 4 +++- examples/papi/sc-team-drive.js | 4 +++- examples/papi/sc-token-gated.js | 4 +++- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/papi/sc-api.js b/examples/papi/sc-api.js index 4ebfd688..661909ee 100644 --- a/examples/papi/sc-api.js +++ b/examples/papi/sc-api.js @@ -49,12 +49,12 @@ export function h160ToSubstrate(addressBytes) { * (`h160ToSubstrate(deployed.addressBytes)`) when a contract forwards the * terms. */ -export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, duration }) { +export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, duration, pricePerByte }) { const signed = await negotiateTerms(providerUrl, { owner: owner.address, max_bytes: BigInt(maxBytes), duration, - price_per_byte: 0n, + price_per_byte: price_per_byte, replica_params: null, bucket_id: null, }); diff --git a/examples/papi/sc-coverage.js b/examples/papi/sc-coverage.js index a2b05fcc..412cebab 100644 --- a/examples/papi/sc-coverage.js +++ b/examples/papi/sc-coverage.js @@ -107,8 +107,9 @@ async function main() { // registered by earlier demos in the CI matrix) so only the provider we // negotiate with holds agreements during this run. console.log("\n[setup] provider + account mapping…"); + const PRICE_PER_BYTE = 1n; await ensureProviderRegistered(api, provider, providerUrl, { - pricePerByte: 1n, + pricePerByte: PRICE_PER_BYTE, maxDuration: 100_000, }); await ensureSoleAcceptingProvider(api, provider); @@ -129,7 +130,7 @@ async function main() { const maxBytesA = 2048n; const durationA = 100; const maxPaymentA = maxBytesA * BigInt(durationA) * 10n; // generous - const signedA = await negotiateAbiTerms(client, { maxBytes: maxBytesA, duration: durationA }); + const signedA = await negotiateAbiTerms(client, { maxBytes: maxBytesA, duration: durationA, pricePerByte: PRICE_PER_BYTE }); let nextBucketBefore = await api.query.StorageProvider.NextBucketId.getValue(); let r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "establishStorageAgreement", [ toHex(providerBytes32), @@ -198,7 +199,7 @@ async function main() { // MILLIUNIT = 1e9 atomic). 10% of `1MiB × 100k blocks × 1` ≈ 1e10 atomic, // comfortably above ED. console.log("\n[7] IWeb3Storage.establishStorageAgreement(provider, terms[1MiB×100k], sig) [burn-sized]"); - const signedB = await negotiateAbiTerms(client, { maxBytes: 1n << 20n, duration: 100_000 }); + const signedB = await negotiateAbiTerms(client, { maxBytes: 1n << 20n, duration: 100_000, pricePerByte: PRICE_PER_BYTE }); nextBucketBefore = await api.query.StorageProvider.NextBucketId.getValue(); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "establishStorageAgreement", [ toHex(providerBytes32), @@ -225,7 +226,7 @@ async function main() { // challenge. The agreement is left open and is not ended; settlement // happens through chain-driven expiry, not this test. console.log("\n[9] IWeb3Storage.establishStorageAgreement(provider, terms[2KiB×100], sig) [freeze/challenge target]"); - const signedC = await negotiateAbiTerms(client, { maxBytes: 2048n, duration: 100 }); + const signedC = await negotiateAbiTerms(client, { maxBytes: 2048n, duration: 100, pricePerByte: PRICE_PER_BYTE }); nextBucketBefore = await api.query.StorageProvider.NextBucketId.getValue(); r = await callPrecompile(api, client, WEB3_STORAGE_ADDR, iWeb3, "establishStorageAgreement", [ toHex(providerBytes32), @@ -267,7 +268,7 @@ async function main() { // 11. createDrive ----------------------------------------------------- console.log("\n[11] IDriveRegistry.createDrive(\"cov\", provider, terms[1MiB×50], sig)"); - const signedD = await negotiateAbiTerms(client, { maxBytes: 1n << 20n, duration: 50 }); + const signedD = await negotiateAbiTerms(client, { maxBytes: 1n << 20n, duration: 50, pricePerByte: PRICE_PER_BYTE }); const nextDriveBefore = await api.query.DriveRegistry.NextDriveId.getValue(); r = await callPrecompile(api, client, DRIVE_REGISTRY_ADDR, iDrive, "createDrive", [ "cov", diff --git a/examples/papi/sc-flow.js b/examples/papi/sc-flow.js index 2b00dfd3..7b10cf76 100644 --- a/examples/papi/sc-flow.js +++ b/examples/papi/sc-flow.js @@ -95,8 +95,9 @@ async function main() { // (`map_account`) — substrate-native accounts can't dispatch contract // calls or be the target of value transfers until they're mapped. console.log("\n[1/6] Provider setup + Revive account mapping…"); + const PRICE_PER_BYTE = 1n; await ensureProviderRegistered(api, provider, PROVIDER_URL, { - pricePerByte: 1n, + pricePerByte: PRICE_PER_BYTE, maxDuration: 100_000, }); await ensureSoleAcceptingProvider(api, provider); @@ -132,6 +133,7 @@ async function main() { const signed = await negotiatePrecompileTerms(PROVIDER_URL, contractAccount, { maxBytes: MAX_BYTES, duration: DURATION, + pricePerByte: PRICE_PER_BYTE, }); const buyData = encodeCall(abi, "buyStorage", [ toHex(provider.publicKey), diff --git a/examples/papi/sc-team-drive.js b/examples/papi/sc-team-drive.js index a42ac28c..d11798bb 100644 --- a/examples/papi/sc-team-drive.js +++ b/examples/papi/sc-team-drive.js @@ -71,8 +71,9 @@ async function main() { const member = makeSigner("//Charlie"); console.log("\n[setup] provider + Revive account mapping…"); + const PRICE_PER_BYTE = 1n; await ensureProviderRegistered(api, provider, providerUrl, { - pricePerByte: 1n, + pricePerByte: PRICE_PER_BYTE, maxDuration: 100_000, }); await ensureSoleAcceptingProvider(api, provider); @@ -106,6 +107,7 @@ async function main() { const signed = await negotiatePrecompileTerms(providerUrl, contractAccount, { maxBytes: 1n << 20n, // 1 MiB capacity duration: 50, + pricePerByte: PRICE_PER_BYTE, }); const createData = encodeCall(abi, "createTeam", [ "team-cov", diff --git a/examples/papi/sc-token-gated.js b/examples/papi/sc-token-gated.js index 3ed62da5..51a1de1b 100644 --- a/examples/papi/sc-token-gated.js +++ b/examples/papi/sc-token-gated.js @@ -80,8 +80,9 @@ async function main() { const recipientH160 = substrateToH160(recipient.publicKey); console.log("\n[setup] provider + Revive account mapping…"); + const PRICE_PER_BYTE = 1n; await ensureProviderRegistered(api, provider, providerUrl, { - pricePerByte: 1n, + pricePerByte: PRICE_PER_BYTE, maxDuration: 100_000, }); await ensureSoleAcceptingProvider(api, provider); @@ -118,6 +119,7 @@ async function main() { const signed = await negotiatePrecompileTerms(providerUrl, contractAccount, { maxBytes: 1n << 20n, duration: 50, + pricePerByte: PRICE_PER_BYTE, }); const initData = encodeCall(abi, "initialize", [ bucketName, From 2abee61b5361aac2c3d43eb11b47fb8e61c2d469 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:58:08 +0700 Subject: [PATCH 41/44] fix: update sc-api to ensure negotiate request body --- examples/papi/sc-api.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/papi/sc-api.js b/examples/papi/sc-api.js index 661909ee..1179d9d2 100644 --- a/examples/papi/sc-api.js +++ b/examples/papi/sc-api.js @@ -54,7 +54,7 @@ export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, d owner: owner.address, max_bytes: BigInt(maxBytes), duration, - price_per_byte: price_per_byte, + price_per_byte: pricePerByte, replica_params: null, bucket_id: null, }); @@ -77,9 +77,9 @@ export async function negotiatePrecompileTerms(providerUrl, owner, { maxBytes, d hasBucketId: bucket != null, bucketId: BigInt(bucket ?? 0), }, - // Hex SCALE-encoded MultiSignature (variant byte + raw sig) — passed - // through verbatim as the `bytes signature` ABI param. - signature: signed.signature, + signature: signed.signature.startsWith("0x") + ? signed.signature + : `0x${signed.signature}`, }; } From a6f6a993df3504f8a28fa379304da5313b3cd600 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 19:29:39 +0700 Subject: [PATCH 42/44] fix: update ci examples --- Cargo.lock | 1 + Cargo.toml | 1 + client/Cargo.toml | 1 + client/src/agreement.rs | 56 +++++++++++++------ client/src/substrate.rs | 12 ++++ client/tests/common/mod.rs | 2 +- .../client/examples/ci_integration_test.rs | 4 +- .../file-system/client/src/lib.rs | 2 +- .../file-system/pallet-registry/src/tests.rs | 2 +- .../s3/client/examples/basic_usage.rs | 2 +- .../s3/client/examples/ci_integration_test.rs | 4 +- .../s3/pallet-s3-registry/src/tests.rs | 2 +- 12 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c759b93..e94ec91b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9555,6 +9555,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_with", "sp-core", "sp-runtime", "storage-primitives", diff --git a/Cargo.toml b/Cargo.toml index 028a1ae8..95753675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ scale-info = { version = "2.11.6", default-features = false, features = [ # External dependencies serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = { version = "1.0", default-features = false } +serde_with = { version = "3.20.0" } tracing = { version = "0.1.41", default-features = false } tracing-subscriber = { version = "=0.3.19" } diff --git a/client/Cargo.toml b/client/Cargo.toml index a6093a85..2c998260 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -13,6 +13,7 @@ codec = { workspace = true, features = ["std"] } reqwest = { workspace = true } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true } +serde_with = { workspace = true } sp-core = { workspace = true, features = ["std"] } sp-runtime = { workspace = true, features = ["std"] } tokio = { workspace = true } diff --git a/client/src/agreement.rs b/client/src/agreement.rs index 904c6129..2a8d0411 100644 --- a/client/src/agreement.rs +++ b/client/src/agreement.rs @@ -16,7 +16,8 @@ //! public key, so the same payload has to be built on both sides — //! `sign_terms` enforces that via [`AgreementTerms::signing_payload`]. -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr, PickFirst}; use sp_core::hashing::blake2_256; use sp_runtime::{AccountId32, MultiSignature}; use storage_primitives::{AgreementTerms, BucketId}; @@ -35,19 +36,20 @@ pub type ReplicaTermsOf = storage_primitives::ReplicaTerms; /// allocates a fresh nonce and a validity window from its own state, /// builds the full [`AgreementTermsOf`], signs it, and returns /// [`SignedTerms`]. +#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NegotiateRequest { /// Account that will own the resulting bucket. pub owner: AccountId32, /// Storage quota requested, in bytes. /// FIX: Safely handles the JS BigInt sent as a string - #[serde(deserialize_with = "deserialize_number_from_string_or_number")] + #[serde_as(as = "PickFirst<(DisplayFromStr, _)>")] pub max_bytes: u64, /// Agreement duration in blocks from activation. pub duration: u32, /// Price per byte per block the owner is willing to lock in. /// FIX: Safely handles the JS BigInt sent as a string - #[serde(deserialize_with = "deserialize_number_from_string_or_number")] + #[serde_as(as = "PickFirst<(DisplayFromStr, _)>")] pub price_per_byte: u128, /// Bucket the quote is bound to. /// - `None` for primary terms; @@ -105,22 +107,40 @@ mod hex_multi_signature { } } -// Universal helper function to accept either a JSON string or raw JSON number -fn deserialize_number_from_string_or_number<'de, T, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, - T: std::str::FromStr + Deserialize<'de>, - ::Err: std::fmt::Display, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrNumber { - String(String), - Number(T), +#[cfg(test)] +mod tests { + use super::*; + + // Rust clients serialize `max_bytes`/`price_per_byte` as raw JSON + // numbers. Regression test: the previous untagged-enum deserializer + // rejected any JSON number for the u128 field (serde's untagged + // buffering has no 128-bit support), surfacing as a 422 from + // `/negotiate` for every Rust caller. + #[test] + fn negotiate_request_roundtrips_rust_numbers() { + let req = NegotiateRequest { + owner: AccountId32::new([0u8; 32]), + max_bytes: 1_000_000_000, + duration: 500, + price_per_byte: 1, + bucket_id: None, + replica_params: None, + }; + let json = serde_json::to_string(&req).unwrap(); + let decoded: NegotiateRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.max_bytes, req.max_bytes); + assert_eq!(decoded.price_per_byte, req.price_per_byte); } - match StringOrNumber::deserialize(deserializer)? { - StringOrNumber::String(s) => s.parse::().map_err(serde::de::Error::custom), - StringOrNumber::Number(n) => Ok(n), + // JS clients send BigInt fields as decimal strings (commit 17528eb). + #[test] + fn negotiate_request_accepts_js_bigint_strings() { + let json = format!( + r#"{{"owner":"{}","max_bytes":"1073741824","duration":50,"price_per_byte":"340282366920938463463374607431768211455","bucket_id":null,"replica_params":null}}"#, + AccountId32::new([0u8; 32]) + ); + let decoded: NegotiateRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.max_bytes, 1_073_741_824); + assert_eq!(decoded.price_per_byte, u128::MAX); } } diff --git a/client/src/substrate.rs b/client/src/substrate.rs index 7d1fda5e..db63b866 100644 --- a/client/src/substrate.rs +++ b/client/src/substrate.rs @@ -203,6 +203,13 @@ pub mod extrinsics { ])], ), }; + let bucket_id_value = match terms.bucket_id { + None => subxt::dynamic::Value::unnamed_variant("None", vec![]), + Some(id) => subxt::dynamic::Value::unnamed_variant( + "Some", + vec![subxt::dynamic::Value::u128(id as u128)], + ), + }; subxt::dynamic::Value::named_composite([ ( "owner", @@ -225,11 +232,16 @@ pub mod extrinsics { subxt::dynamic::Value::u128(terms.valid_until as u128), ), ("nonce", subxt::dynamic::Value::u128(terms.nonce as u128)), + ("bucket_id", bucket_id_value), ("replica_params", replica_params_value), ]) } /// Encode a [`sp_runtime::MultiSignature`] as a subxt dynamic variant. + /// + /// Variant names and payloads mirror the pallet's + /// `verify_terms_signature` match: the signature travels as the + /// variant's raw inner bytes. pub fn dynamic_multi_signature(sig: &sp_runtime::MultiSignature) -> subxt::dynamic::Value { let (variant, bytes) = match sig { sp_runtime::MultiSignature::Sr25519(s) => ("Sr25519", s.encode()), diff --git a/client/tests/common/mod.rs b/client/tests/common/mod.rs index f48c5c5f..6a775312 100644 --- a/client/tests/common/mod.rs +++ b/client/tests/common/mod.rs @@ -166,7 +166,7 @@ pub async fn chain_setup() -> Option { owner: dev_account("alice"), max_bytes: 1_000_000, duration: 100, - price_per_byte: 0, + price_per_byte: 1, valid_until: u32::MAX, nonce, replica_params: None, diff --git a/storage-interfaces/file-system/client/examples/ci_integration_test.rs b/storage-interfaces/file-system/client/examples/ci_integration_test.rs index f381284f..5e7851b6 100644 --- a/storage-interfaces/file-system/client/examples/ci_integration_test.rs +++ b/storage-interfaces/file-system/client/examples/ci_integration_test.rs @@ -84,9 +84,9 @@ async fn main() -> Result<(), Box> { owner, max_bytes: 1_000_000_000, // 1 GB duration: 500, // 500 blocks - price_per_byte: 0, - replica_params: None, + price_per_byte: 1, bucket_id: None, + replica_params: None, }, ) .await?; diff --git a/storage-interfaces/file-system/client/src/lib.rs b/storage-interfaces/file-system/client/src/lib.rs index 0fc6624b..4ec73d30 100644 --- a/storage-interfaces/file-system/client/src/lib.rs +++ b/storage-interfaces/file-system/client/src/lib.rs @@ -191,7 +191,7 @@ impl FileSystemClient { /// owner: owner_account, /// max_bytes: 10_000_000_000, /// duration: 500, - /// price_per_byte: 0, + /// price_per_byte: 1, /// replica_params: None, /// }, /// ).await?; diff --git a/storage-interfaces/file-system/pallet-registry/src/tests.rs b/storage-interfaces/file-system/pallet-registry/src/tests.rs index 71cf1740..54613cf2 100644 --- a/storage-interfaces/file-system/pallet-registry/src/tests.rs +++ b/storage-interfaces/file-system/pallet-registry/src/tests.rs @@ -35,7 +35,7 @@ fn primary_terms(owner: u64, max_bytes: u64, duration: u64, nonce: u64) -> Agree owner, max_bytes, duration, - price_per_byte: 0u128, + price_per_byte: 1u128, valid_until: 1_000_000u64, nonce, bucket_id: None, diff --git a/storage-interfaces/s3/client/examples/basic_usage.rs b/storage-interfaces/s3/client/examples/basic_usage.rs index c0161166..91b6228a 100644 --- a/storage-interfaces/s3/client/examples/basic_usage.rs +++ b/storage-interfaces/s3/client/examples/basic_usage.rs @@ -56,7 +56,7 @@ async fn main() -> Result<(), Box> { owner, max_bytes: 1_000_000_000, // 1 GB duration: 500, // 500 blocks - price_per_byte: 0, + price_per_byte: 1, replica_params: None, bucket_id: None, }, diff --git a/storage-interfaces/s3/client/examples/ci_integration_test.rs b/storage-interfaces/s3/client/examples/ci_integration_test.rs index f7198cc1..0a7317e6 100644 --- a/storage-interfaces/s3/client/examples/ci_integration_test.rs +++ b/storage-interfaces/s3/client/examples/ci_integration_test.rs @@ -61,9 +61,9 @@ async fn main() -> Result<(), Box> { owner, max_bytes: 1_000_000_000, // 1 GB duration: 500, // 500 blocks - price_per_byte: 0, - replica_params: None, + price_per_byte: 1, bucket_id: None, + replica_params: None, }, ) .await?; diff --git a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs index 2ff469a5..f73ef09a 100644 --- a/storage-interfaces/s3/pallet-s3-registry/src/tests.rs +++ b/storage-interfaces/s3/pallet-s3-registry/src/tests.rs @@ -39,7 +39,7 @@ fn primary_terms(owner: u64, max_bytes: u64, duration: u64, nonce: u64) -> Agree owner, max_bytes, duration, - price_per_byte: 0u128, + price_per_byte: 1u128, valid_until: 1_000_000u64, nonce, bucket_id: None, From 86917bbf759d806ea48e668138e2e2d95fa830a0 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:01:02 +0700 Subject: [PATCH 43/44] ci: register provider on-chain before starting provider node in ui-e2e --- .github/workflows/ui-e2e.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ui-e2e.yml b/.github/workflows/ui-e2e.yml index 06d6ed26..e12a95d7 100644 --- a/.github/workflows/ui-e2e.yml +++ b/.github/workflows/ui-e2e.yml @@ -118,14 +118,18 @@ jobs: - name: Wait for parachain blocks uses: ./.github/actions/wait-for-parachain - - name: Start provider (Alice, inmemory) and wait for health + - name: Register provider on-chain run: | echo "//Alice" > /tmp/alice-key && chmod 600 /tmp/alice-key + cargo run --release -p storage-client --example register_provider \ + ws://127.0.0.1:2222 http://127.0.0.1:3333 /ip4/127.0.0.1/tcp/3333 /tmp/alice-key + + - name: Start provider (Alice, inmemory) and wait for health + run: | nohup ./target/release/storage-provider-node \ --keyfile /tmp/alice-key --storage-mode inmemory \ --bind-addr 0.0.0.0:3333 --chain-rpc ws://127.0.0.1:2222 \ - --enable-agreement-coordinator --enable-checkpoint-coordinator \ - > /tmp/provider.log 2>&1 & + --enable-checkpoint-coordinator > /tmp/provider.log 2>&1 & for i in $(seq 1 60); do if curl -s http://127.0.0.1:3333/health | jq -e '.status' > /dev/null 2>&1; then echo "Provider healthy (attempt $i)" From e7ef4a00b230f6ea21cd0b347914e83dd4547330 Mon Sep 17 00:00:00 2001 From: Daniel Bui <79790753+danielbui12@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:20:30 +0700 Subject: [PATCH 44/44] fix: using runtime api to query matching provider & retrieving bucket id from event --- .../e2e/helpers/createBucketViaUi.ts | 5 +- .../e2e/helpers/switchAccountToNonProvider.ts | 18 +++ .../e2e/integration/bucket-create.spec.ts | 50 ++++++--- .../e2e/integration/encryption.spec.ts | 9 +- .../e2e/integration/members.spec.ts | 4 +- .../e2e/integration/s3-objects.spec.ts | 4 +- .../src/components/ProviderPickerDialog.tsx | 104 ++++++++++++++---- .../console-ui/src/components/S3Tab.tsx | 1 + .../console-ui/src/hooks/useStorage.tsx | 15 +++ user-interfaces/console-ui/src/lib/storage.ts | 83 ++++++++++++++ .../drive-ui/e2e/helpers/createDriveViaUi.ts | 49 +++++---- .../e2e/integration/drive-create.spec.ts | 32 ++---- .../drive-ui/e2e/integration/members.spec.ts | 6 +- .../drive-ui/e2e/integration/realtime.spec.ts | 1 + .../src/components/NewDriveDialog.tsx | 24 ++-- .../src/components/ProviderPickerPanel.tsx | 60 ++++++++-- .../drive-ui/src/lib/drive-client.ts | 82 +++++++++++++- .../drive-ui/src/state/drive.state.ts | 13 ++- .../shared/test-helpers/src/buckets.ts | 5 + .../shared/test-helpers/src/fixtures.ts | 2 +- .../shared/test-helpers/src/setupProvider.ts | 19 ++++ 21 files changed, 458 insertions(+), 128 deletions(-) create mode 100644 user-interfaces/console-ui/e2e/helpers/switchAccountToNonProvider.ts create mode 100644 user-interfaces/shared/test-helpers/src/setupProvider.ts diff --git a/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts b/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts index e6aad842..4737b642 100644 --- a/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts +++ b/user-interfaces/console-ui/e2e/helpers/createBucketViaUi.ts @@ -21,6 +21,7 @@ export async function createBucketViaUi(page: Page, name: string): Promise await expect(page.getByTestId("s3-create-bucket-form")).toBeVisible(); await page.getByTestId("s3-bucket-name-input").fill(name); + await page.getByTestId("s3-bucket-price-input").fill("100"); await page.getByTestId("s3-create-submit").click(); await expect(page.getByTestId("provider-picker")).toBeVisible({ timeout: 30_000 }); @@ -42,10 +43,10 @@ export async function createBucketInFreshContext( name: string, ): Promise { const context = await browser.newContext(); - await context.addInitScript(() => { + const page = await context.newPage(); + await page.addInitScript(() => { localStorage.setItem("web3-storage-selected-network", "local"); }); - const page = await context.newPage(); try { await page.goto("/"); await expect(page.getByTestId("block-number")).toBeVisible({ timeout: 30_000 }); diff --git a/user-interfaces/console-ui/e2e/helpers/switchAccountToNonProvider.ts b/user-interfaces/console-ui/e2e/helpers/switchAccountToNonProvider.ts new file mode 100644 index 00000000..57e0e070 --- /dev/null +++ b/user-interfaces/console-ui/e2e/helpers/switchAccountToNonProvider.ts @@ -0,0 +1,18 @@ +import { expect, type Page } from "@playwright/test"; + +export const switchAccountHandler = async (localPage: Page, who: string = "Bob") => { + await localPage.getByTestId("nav-accounts").click(); + await expect(localPage.getByTestId("accounts-list")).toBeVisible(); + + // Default active should be Alice (auto-set on local). + await expect(localPage.getByTestId("accounts-active-badge-Alice")).toBeVisible({ + timeout: 30_000, + }); + + await localPage.getByTestId("accounts-set-active-Bob").click(); + await expect(localPage.getByTestId("accounts-active-badge-Bob")).toBeVisible({ + timeout: 30_000, + }); + // Sidebar signer-name reflects the new account. + await expect(localPage.getByTestId("signer-name")).toHaveText("Bob"); +} \ No newline at end of file diff --git a/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts b/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts index 3de29ba7..06c8b08e 100644 --- a/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/bucket-create.spec.ts @@ -23,7 +23,7 @@ test.setTimeout(180_000); // per-spec beforeAll registration needed. test.afterAll(async () => { - test.setTimeout(90_000); + test.setTimeout(180_000); await cleanupBuckets(Alice); }); @@ -37,7 +37,8 @@ test("create bucket via UI → on-chain S3Buckets matches", async ({ localPage } const name = `bucket-create-${Date.now()}`; await localPage.getByTestId("s3-bucket-name-input").fill(name); - // Defaults for capacity / duration / price-per-byte are set in the component. + await localPage.getByTestId("s3-bucket-price-input").fill("100"); + // Defaults for capacity / duration are set in the component. await localPage.getByTestId("s3-create-submit").click(); // Step 1 of the new flow: the picker opens with the list of available @@ -46,6 +47,28 @@ test("create bucket via UI → on-chain S3Buckets matches", async ({ localPage } await expect(localPage.getByTestId("provider-picker")).toBeVisible({ timeout: 30_000, }); + + // Subscribe to S3BucketCreated BEFORE triggering the on-chain submit so we + // don't miss the block. Resolve with the s3_bucket_id of the event whose + // name + owner match this test's bucket. test-helpers' getApi is a separate + // ws connection, so subscribing here is the reliable way to observe the + // event the UI's own (separate) connection submits. + const api = getApi(); + const createdBucketId = new Promise((resolve, reject) => { + const sub = api.event.S3Registry.S3BucketCreated.watch().subscribe({ + next: ({ events }) => { + for (const { payload } of events) { + const eventName = new TextDecoder().decode(payload.name); + if (eventName === name) { + sub.unsubscribe(); + resolve(payload.s3_bucket_id); + } + } + }, + error: reject, + }); + }); + await localPage.getByTestId("provider-picker-select").first().click(); // Step 2 runs automatically (HTTP negotiate → on-chain submit). Wait for @@ -55,22 +78,15 @@ test("create bucket via UI → on-chain S3Buckets matches", async ({ localPage } timeout: 90_000, }); - // Verify on chain. The UI submits at best-block, but test-helpers' getApi - // is a separate ws connection — finalization can lag, so poll instead of - // doing an immediate getValue. - const api = getApi(); - await expect.poll( - async () => { - const ids = await api.query.S3Registry.UserBuckets.getValue(Alice.address); - return ids?.length ?? 0; - }, - { timeout: 60_000, intervals: [1000, 2000, 3000] }, - ).toBeGreaterThan(0); - - const userBuckets = await api.query.S3Registry.UserBuckets.getValue(Alice.address); - const latestId = userBuckets![userBuckets!.length - 1]; - const bucket = await api.query.S3Registry.S3Buckets.getValue(latestId); + // The event subscriber gives us the authoritative s3_bucket_id minted by the + // runtime. Verify the matching on-chain record carries our bucket name. + const s3BucketId = await createdBucketId; + const bucket = await api.query.S3Registry.S3Buckets.getValue(s3BucketId); expect(bucket).toBeTruthy(); const bucketName = new TextDecoder().decode(bucket!.name); expect(bucketName).toBe(name); + + // And that it's tracked under the owner's bucket list. + const userBuckets = await api.query.S3Registry.UserBuckets.getValue(Alice.address); + expect(userBuckets?.map((id) => id.toString())).toContain(s3BucketId.toString()); }); diff --git a/user-interfaces/console-ui/e2e/integration/encryption.spec.ts b/user-interfaces/console-ui/e2e/integration/encryption.spec.ts index 7335e6e6..7bd60e4d 100644 --- a/user-interfaces/console-ui/e2e/integration/encryption.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/encryption.spec.ts @@ -9,21 +9,18 @@ import { Alice, cleanupBuckets } from "@web3-storage/test-helpers"; import { createBucketInFreshContext } from "../helpers/createBucketViaUi"; test.describe.configure({ mode: "serial" }); -test.setTimeout(120_000); +test.setTimeout(180_000); let bucketName: string; test.beforeAll(async ({ browser }) => { - test.setTimeout(120_000); + test.setTimeout(180_000); bucketName = `enc-${Date.now()}`; await createBucketInFreshContext(browser, bucketName); }); test.afterAll(async () => { - // cleanupBuckets awaits finalization per bucket (~12-24s) and Alice may - // have accumulated buckets when an earlier spec's afterAll missed — - // give it room rather than the playwright default of 30s. - test.setTimeout(90_000); + test.setTimeout(180_000); await cleanupBuckets(Alice); }); diff --git a/user-interfaces/console-ui/e2e/integration/members.spec.ts b/user-interfaces/console-ui/e2e/integration/members.spec.ts index db6aef5b..338c429a 100644 --- a/user-interfaces/console-ui/e2e/integration/members.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/members.spec.ts @@ -15,13 +15,13 @@ test.setTimeout(180_000); let bucketName: string; test.beforeAll(async ({ browser }) => { - test.setTimeout(120_000); + test.setTimeout(180_000); bucketName = `members-${Date.now()}`; await createBucketInFreshContext(browser, bucketName); }); test.afterAll(async () => { - test.setTimeout(90_000); + test.setTimeout(180_000); await cleanupBuckets(Alice); }); diff --git a/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts b/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts index 747a7b15..a0b42eb0 100644 --- a/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts +++ b/user-interfaces/console-ui/e2e/integration/s3-objects.spec.ts @@ -15,13 +15,13 @@ test.setTimeout(180_000); let bucketName: string; test.beforeAll(async ({ browser }) => { - test.setTimeout(120_000); + test.setTimeout(180_000); bucketName = `objects-${Date.now()}`; await createBucketInFreshContext(browser, bucketName); }); test.afterAll(async () => { - test.setTimeout(90_000); + test.setTimeout(180_000); await cleanupBuckets(Alice); }); diff --git a/user-interfaces/console-ui/src/components/ProviderPickerDialog.tsx b/user-interfaces/console-ui/src/components/ProviderPickerDialog.tsx index a79b1cac..32a570f8 100644 --- a/user-interfaces/console-ui/src/components/ProviderPickerDialog.tsx +++ b/user-interfaces/console-ui/src/components/ProviderPickerDialog.tsx @@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com import { RefreshCw, X, Server } from "lucide-react"; import { useStorage } from "@/hooks/useStorage"; import { formatBytes, truncateHash, formatTokens } from "@/lib/utils"; -import type { AvailableProvider } from "@/lib/storage"; +import type { AvailableProvider, MatchingProviders } from "@/lib/storage"; interface ProviderPickerDialogProps { open: boolean; @@ -12,6 +12,7 @@ interface ProviderPickerDialogProps { onSelect: (provider: AvailableProvider) => void; requiredCapacity: bigint; requiredDuration: number; + requiredPricePerByte: bigint; } export default function ProviderPickerDialog({ @@ -20,9 +21,10 @@ export default function ProviderPickerDialog({ onSelect, requiredCapacity, requiredDuration, + requiredPricePerByte, }: ProviderPickerDialogProps) { - const { listAvailableProviders } = useStorage(); - const [providers, setProviders] = useState([]); + const { queryMatchingProviders } = useStorage(); + const [providers, setProviders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -36,7 +38,12 @@ export default function ProviderPickerDialog({ setLoading(true); setError(null); try { - const list = await listAvailableProviders(); + const list = await queryMatchingProviders({ + primaryOnly: true, + maxPricePerByte: requiredPricePerByte, + bytesNeeded: requiredCapacity, + minDuration: requiredDuration, + }); setProviders(list); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load providers"); @@ -47,9 +54,25 @@ export default function ProviderPickerDialog({ if (!open) return null; + const humanizeReason = (reason: string): string => { + switch (reason) { + case "PriceTooHigh": + return "Price above your max"; + case "InsufficientCapacity": + return "Limited capacity"; + case "DurationMismatch": + return "Duration mismatch"; + case "NotAccepting": + return "Not accepting primary"; + default: + return reason; + } + }; + const getDisabledReason = (p: AvailableProvider): string | null => { if (!p.acceptingPrimary) return "Not accepting"; - if (p.availableCapacity !== 0n && p.availableCapacity < requiredCapacity) return "Capacity full"; + // `maxCapacity === 0n` means unlimited — skip the capacity check. + if (p.maxCapacity !== 0n && p.availableCapacity < requiredCapacity) return "Capacity full"; if (requiredDuration < p.minDuration) return "Duration too short"; if (p.maxDuration > 0 && requiredDuration > p.maxDuration) return "Duration too long"; return null; @@ -101,6 +124,9 @@ export default function ProviderPickerDialog({ Account + + Match + Available @@ -111,7 +137,7 @@ export default function ProviderPickerDialog({ Duration - Agreements + Reputation Action @@ -122,13 +148,15 @@ export default function ProviderPickerDialog({ {providers.map((p) => { const reason = getDisabledReason(p); const disabled = reason !== null; - const utilization = - p.maxCapacity > 0n - ? Number( - ((p.maxCapacity - p.availableCapacity) * 100n) / - p.maxCapacity, - ) - : 0; + // `max_capacity == 0` is the substrate convention for + // "unlimited" (see runtime ProviderSettings docs). + const unlimited = p.maxCapacity === 0n; + const utilization = unlimited + ? 0 + : Number( + ((p.maxCapacity - p.availableCapacity) * 100n) / + p.maxCapacity, + ); return ( -
- - {formatBytes(Number(p.availableCapacity))} +
+ {p.matchScore}/100 +
+ {p.partialReason ? ( +
+ {humanizeReason(p.partialReason)} +
+ ) : ( +
+ Best fit +
+ )} + + + {unlimited ? ( + + Unlimited -
-
+ ) : ( +
+ + {formatBytes(Number(p.availableCapacity))} + +
+
+
-
+ )} {formatTokens(p.pricePerByte)} @@ -159,7 +207,17 @@ export default function ProviderPickerDialog({ {p.minDuration}–{p.maxDuration || "\u221E"} - {p.agreementsTotal} +
{p.agreementsTotal} agreements
+
0 + ? "text-red-500" + : "text-muted-foreground" + } + > + {p.challengesFailed}/{p.challengesReceived} challenges + failed +
{disabled ? ( diff --git a/user-interfaces/console-ui/src/components/S3Tab.tsx b/user-interfaces/console-ui/src/components/S3Tab.tsx index 51144307..89433727 100644 --- a/user-interfaces/console-ui/src/components/S3Tab.tsx +++ b/user-interfaces/console-ui/src/components/S3Tab.tsx @@ -532,6 +532,7 @@ export default function S3Tab({ onBucketSelect }: S3TabProps) { onSelect={handleProviderSelect} requiredCapacity={BigInt(bucketCapacity)} requiredDuration={parseInt(bucketDuration, 10)} + requiredPricePerByte={BigInt(bucketPricePerByte || "0")} /> )} diff --git a/user-interfaces/console-ui/src/hooks/useStorage.tsx b/user-interfaces/console-ui/src/hooks/useStorage.tsx index 40d80726..ea7dd613 100644 --- a/user-interfaces/console-ui/src/hooks/useStorage.tsx +++ b/user-interfaces/console-ui/src/hooks/useStorage.tsx @@ -12,6 +12,8 @@ import { type BucketMember, type ProviderEndpointInfo, type AvailableProvider, + type MatchingProviders, + type QueryMatchingProvidersParams, type SignedTerms, type UploadResult, type PutObjectOptions, @@ -53,6 +55,10 @@ interface StorageState { // Provider checkProviderHealth: (bucketId: bigint) => Promise; listAvailableProviders: () => Promise; + queryMatchingProviders: ( + query: QueryMatchingProvidersParams["query"], + limit?: QueryMatchingProvidersParams["limit"], + ) => Promise; // Bucket Members & Permissions fetchBucketMembers: (bucketId: bigint) => Promise; @@ -308,6 +314,14 @@ export function StorageProvider({ children }: { children: ReactNode }) { return client.listAvailableProviders(); }, [client]); + const queryMatchingProviders = useCallback(async ( + query: QueryMatchingProvidersParams["query"], + limit?: QueryMatchingProvidersParams["limit"], + ): Promise => { + if (!client) throw new Error("Client not connected"); + return client.queryMatchingProviders(query, limit); + }, [client]); + // --- Bucket Members & Permissions --- const fetchBucketMembers = useCallback(async (bucketId: bigint): Promise => { @@ -451,6 +465,7 @@ export function StorageProvider({ children }: { children: ReactNode }) { deleteObject, checkProviderHealth, listAvailableProviders, + queryMatchingProviders, fetchBucketMembers, addBucketMember, removeBucketMember, diff --git a/user-interfaces/console-ui/src/lib/storage.ts b/user-interfaces/console-ui/src/lib/storage.ts index 17199daa..c7800304 100644 --- a/user-interfaces/console-ui/src/lib/storage.ts +++ b/user-interfaces/console-ui/src/lib/storage.ts @@ -216,6 +216,30 @@ export interface AvailableProvider { agreementsTotal: number; } +export interface MatchingProviders extends AvailableProvider { + matchScore: number; + partialReason: string; + committedBytes: bigint; + replicaSyncPrice?: bigint; + acceptingExtensions: boolean; + registeredAt: number; + agreementsExtended: number; + agreementsNotExtended: number; + agreementsBurned: number; + challengesReceived: number; + challengesFailed: number; +} + +export interface QueryMatchingProvidersParams { + query: { + bytesNeeded: bigint; + minDuration: number; + maxPricePerByte: bigint; + primaryOnly: boolean; + }; + limit: number; +} + export interface PutObjectOptions { contentType?: string; metadata?: Record; @@ -1091,6 +1115,65 @@ export class StorageClient { return providers; } + /** + * Score registered providers against the given storage requirements using + * the `find_matching_providers` runtime API. Unlike `listAvailableProviders` + * (a raw storage sweep), this returns a `matchScore` and `partialReason` per + * provider so the picker can surface "best fit" vs near-misses. Sorted by + * score descending. + */ + async queryMatchingProviders( + query: QueryMatchingProvidersParams["query"], + limit: QueryMatchingProvidersParams["limit"] = 10, + ): Promise { + if (!this.api) throw new Error("Not connected. Call connect() first."); + + const matches = await this.api.apis.StorageProviderApi.find_matching_providers( + { + bytes_needed: query.bytesNeeded, + min_duration: query.minDuration, + max_price_per_byte: query.maxPricePerByte, + primary_only: query.primaryOnly, + }, + limit, + ); + + return matches + .map((match) => { + const info = match.info; + const maxCapacity = BigInt(info.max_capacity ?? 0); + const committedBytes = BigInt(info.committed_bytes ?? 0); + const availableCapacity = + maxCapacity > committedBytes ? maxCapacity - committedBytes : 0n; + + return { + account: toSs58(match.account), + multiaddr: new TextDecoder().decode(info.multiaddr), + stake: BigInt(info.stake ?? 0), + availableCapacity, + maxCapacity, + committedBytes, + pricePerByte: BigInt(info.price_per_byte ?? 0), + minDuration: info.min_duration ?? 0, + maxDuration: info.max_duration ?? 0, + acceptingPrimary: info.accepting_primary ?? false, + replicaSyncPrice: + info.replica_sync_price != null ? BigInt(info.replica_sync_price) : undefined, + acceptingExtensions: info.accepting_extensions ?? false, + registeredAt: Number(info.registered_at ?? 0), + agreementsTotal: info.agreements_total ?? 0, + agreementsExtended: info.agreements_extended ?? 0, + agreementsNotExtended: info.agreements_not_extended ?? 0, + agreementsBurned: info.agreements_burned ?? 0, + challengesReceived: info.challenges_received ?? 0, + challengesFailed: info.challenges_failed ?? 0, + matchScore: match.match_score, + partialReason: match.partial_reason?.type ?? "", + }; + }) + .sort((a, b) => b.matchScore - a.matchScore); + } + // --- Helpers --- diff --git a/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts b/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts index f3e47063..69612797 100644 --- a/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts +++ b/user-interfaces/drive-ui/e2e/helpers/createDriveViaUi.ts @@ -1,5 +1,5 @@ import { expect, type Browser, type Page } from "@playwright/test"; -import { Bob, getApi } from "@web3-storage/test-helpers"; +import { getApi } from "@web3-storage/test-helpers"; /** * Drive the drive-ui through the real user create-drive flow: @@ -19,11 +19,17 @@ export async function createDriveViaUi(page: Page, name: string): Promise { const context = await browser.newContext(); - await context.addInitScript(() => { + const page = await context.newPage(); + await page.addInitScript(() => { localStorage.setItem("web3-storage-selected-network", "local"); localStorage.setItem("drive-ui-account-name", "Bob"); }); - const page = await context.newPage(); try { await page.goto("/"); await expect(page.getByTestId("block-number")).toBeVisible({ timeout: 30_000 }); - return await createDriveViaUi(page, name); + return createDriveViaUi(page, name); } finally { await context.close(); } } /** - * Poll `DriveRegistry.UserDrives[Bob]` until it has at least one entry, - * then return the latest id. Used after the UI flow signals completion - * to bridge the UI's optimistic state and the chain's finalized state. + * Resolve with the `drive_id` of the next `DriveRegistry.DriveCreated` event. + * Only Bob creates drives in the test, so the first one is ours. Call this + * BEFORE triggering the create so `.watch()` (forward-only) catches the block. */ -async function waitForLatestDriveId(): Promise { +export async function waitForLatestDriveId(): Promise { const api = getApi(); - let length = 0; - await expect - .poll( - async () => { - const ids = await api.query.DriveRegistry.UserDrives.getValue(Bob.address); - length = ids.length; - return length; + return new Promise((resolve, reject) => { + const sub = api.event.DriveRegistry.DriveCreated.watch().subscribe({ + next: ({ events }) => { + if (events.length === 0) return; + sub.unsubscribe(); + resolve(events[0].payload.drive_id); }, - { timeout: 120_000, intervals: [1000, 2000, 3000] }, - ) - .toBeGreaterThan(0); - const ids = await api.query.DriveRegistry.UserDrives.getValue(Bob.address); - return ids[ids.length - 1]; + error: reject, + }); + }); } diff --git a/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts b/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts index c2a27d2a..5d490c59 100644 --- a/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/drive-create.spec.ts @@ -6,11 +6,8 @@ * `name` is the only user-supplied content we can round-trip. */ import { test, expect } from "../fixtures"; -import { - Bob, - cleanupDrives, - getApi, -} from "@web3-storage/test-helpers"; +import { Bob, cleanupDrives, getApi } from "@web3-storage/test-helpers"; +import { waitForLatestDriveId } from '../helpers/createDriveViaUi'; test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); @@ -21,6 +18,7 @@ test.setTimeout(180_000); // nonce, which then refuses to accept_agreement for our drives. test.afterEach(async () => { + test.setTimeout(180_000); await cleanupDrives(Bob); }); @@ -28,25 +26,8 @@ async function fillBaseFields(page: import("@playwright/test").Page, name: strin await page.getByTestId("new-drive-button").click(); await expect(page.getByTestId("new-drive-dialog")).toBeVisible(); await page.getByTestId("new-drive-name").fill(name); - // Capacity / duration / price-per-byte — defaults are fine for these tests. -} - -/** - * Wait for a freshly-created drive to land in Bob's UserDrives. Returns - * the latest drive id. Fresh chain's first drive has id 0n which is falsy - * in JS — poll on `length`, then read the id outside the poll. - */ -async function waitForCreatedDriveId(): Promise { - const api = getApi(); - await expect.poll( - async () => { - const ids = await api.query.DriveRegistry.UserDrives.getValue(Bob.address); - return ids.length; - }, - { timeout: 120_000, intervals: [1000, 2000, 3000] }, - ).toBeGreaterThan(0); - const ids = await api.query.DriveRegistry.UserDrives.getValue(Bob.address); - return ids[ids.length - 1]; + await page.getByTestId("new-drive-price").fill("100"); + // Capacity / duration — defaults are fine for these tests } async function expectDriveOnChain(driveId: bigint, expectedName: string) { @@ -60,6 +41,7 @@ async function expectDriveOnChain(driveId: bigint, expectedName: string) { test("drive lands on chain with the user-supplied name", async ({ localPage }) => { const name = `create-${Date.now()}`; await fillBaseFields(localPage, name); + await localPage.getByTestId("find-matching-providers").click(); // Provider picker is embedded in the create dialog — picking a provider // IS the submit. globalSetup registered Alice as the lone provider, so // the first row is always the one we want. @@ -68,6 +50,6 @@ test("drive lands on chain with the user-supplied name", async ({ localPage }) = }); await localPage.getByTestId("provider-picker-select").first().click(); - const driveId = await waitForCreatedDriveId(); + const driveId = await waitForLatestDriveId(); await expectDriveOnChain(driveId, name); }); diff --git a/user-interfaces/drive-ui/e2e/integration/members.spec.ts b/user-interfaces/drive-ui/e2e/integration/members.spec.ts index 2523b49f..7166b6e7 100644 --- a/user-interfaces/drive-ui/e2e/integration/members.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/members.spec.ts @@ -11,11 +11,7 @@ * still the sole member when the duplicate-check test runs. */ import { test, expect } from "../fixtures"; -import { - Bob, - Charlie, - cleanupDrives, -} from "@web3-storage/test-helpers"; +import { Bob, Charlie, cleanupDrives } from "@web3-storage/test-helpers"; import { createDriveInFreshContext } from "../helpers/createDriveViaUi"; test.describe.configure({ mode: "serial" }); diff --git a/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts b/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts index 1672a972..8293edbb 100644 --- a/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts +++ b/user-interfaces/drive-ui/e2e/integration/realtime.spec.ts @@ -19,6 +19,7 @@ test.describe.configure({ mode: "serial" }); test.setTimeout(180_000); test.afterEach(async () => { + test.setTimeout(180_000); await cleanupDrives(Bob); }); diff --git a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx index 105a25a0..f6ed8f7f 100644 --- a/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx +++ b/user-interfaces/drive-ui/src/components/NewDriveDialog.tsx @@ -107,6 +107,7 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro const [pricePerByte, setPricePerByte] = useState("0"); const [submitting, setSubmitting] = useState(false); const [negotiateError, setNegotiateError] = useState(null); + const [isShowProviderPicker, setIsShowProviderPicker] = useState(false); /** * Clicking a provider's Select button IS the submit action. Negotiate @@ -167,7 +168,7 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro return ( <> - + Create New Drive @@ -219,12 +220,21 @@ export default function NewDriveDialog({ open, onOpenChange }: NewDriveDialogPro
- + { + isShowProviderPicker ? ( + + ) : ( + + ) + } {negotiateError && (

diff --git a/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx b/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx index 285138a1..c6401b3b 100644 --- a/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx +++ b/user-interfaces/drive-ui/src/components/ProviderPickerPanel.tsx @@ -1,15 +1,14 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { RefreshCw, Server } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { listAvailableProviders } from "@/state"; -import type { AvailableProvider } from "@/lib/drive-client"; +import type { AvailableProvider, MatchingProviders } from "@/lib/drive-client"; import { formatBytes, truncateHash, formatTokens } from "@/lib/utils"; - +import { queryMatchingProviders } from "@/state/drive.state"; interface ProviderPickerPanelProps { onSelect: (provider: AvailableProvider) => void; requiredCapacity: bigint; requiredDuration: number; - /** Disable all Select buttons (e.g. while a submission is in flight). */ + requiredPricePerByte: bigint; disabled?: boolean; } @@ -26,9 +25,10 @@ export default function ProviderPickerPanel({ onSelect, requiredCapacity, requiredDuration, + requiredPricePerByte, disabled = false, }: ProviderPickerPanelProps) { - const [providers, setProviders] = useState([]); + const [providers, setProviders] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -36,17 +36,37 @@ export default function ProviderPickerPanel({ loadProviders(); }, []); - const loadProviders = async () => { + const loadProviders = useCallback(async () => { setLoading(true); setError(null); try { - const list = await listAvailableProviders(); + const list = await queryMatchingProviders({ + primaryOnly: true, + maxPricePerByte: requiredPricePerByte, + bytesNeeded: requiredCapacity, + minDuration: requiredDuration, + }); setProviders(list); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load providers"); } finally { setLoading(false); } + }, [requiredCapacity, requiredDuration, requiredPricePerByte]); + + const humanizeReason = (reason: string): string => { + switch (reason) { + case "PriceTooHigh": + return "Price above your max"; + case "InsufficientCapacity": + return "Limited capacity"; + case "DurationMismatch": + return "Duration mismatch"; + case "NotAccepting": + return "Not accepting primary"; + default: + return reason; + } }; const getDisabledReason = (p: AvailableProvider): string | null => { @@ -98,9 +118,11 @@ export default function ProviderPickerPanel({ Account + Match Available Price/byte Duration + Reputation Action @@ -126,6 +148,16 @@ export default function ProviderPickerPanel({ {truncateHash(p.account, 8, 6)} + +

{p.matchScore}/100
+ {p.partialReason ? ( +
+ {humanizeReason(p.partialReason)} +
+ ) : ( +
Best fit
+ )} + {unlimited ? ( Unlimited @@ -149,6 +181,18 @@ export default function ProviderPickerPanel({ {p.minDuration}–{p.maxDuration || "∞"} + +
{p.agreementsTotal} agreements
+
0 + ? "text-red-500" + : "text-muted-foreground" + } + > + {p.challengesFailed}/{p.challengesReceived} challenges failed +
+ {rowDisabled ? ( {reason} diff --git a/user-interfaces/drive-ui/src/lib/drive-client.ts b/user-interfaces/drive-ui/src/lib/drive-client.ts index 9b900b90..d42b1c12 100644 --- a/user-interfaces/drive-ui/src/lib/drive-client.ts +++ b/user-interfaces/drive-ui/src/lib/drive-client.ts @@ -7,7 +7,7 @@ import { Binary, Enum, type PolkadotSigner, type Transaction, type TxFinalizedPayload } from "polkadot-api"; import { parachain } from "@polkadot-api/descriptors"; -import { resolveProviderEndpoint } from "@web3-storage/papi"; +import { resolveProviderEndpoint, toSs58 } from "@web3-storage/papi"; import type { ParachainApi } from "@/state/chain.state"; export type Signer = PolkadotSigner; @@ -68,6 +68,26 @@ export interface AvailableProvider { agreementsTotal: number; } +export interface MatchingProviders extends AvailableProvider { + matchScore: number; + partialReason: string; + stake: bigint, + committedBytes: bigint; + minDuration: number; + maxDuration: number; + acceptingPrimary: boolean; + replicaSyncPrice?: bigint; + acceptingExtensions: boolean; + registeredAt: number; + agreementsTotal: number; + agreementsExtended: number; + agreementsNotExtended: number; + agreementsBurned: number; + challengesReceived: number; + challengesFailed: number; + maxCapacity: bigint; +} + export interface FsEntry { name: string; path: string; @@ -111,6 +131,16 @@ export interface UploadOptions { signal?: AbortSignal; } +export interface QueryMatchingProvidersParams { + query: { + bytesNeeded: bigint, + minDuration: number, + maxPricePerByte: bigint, + primaryOnly: boolean, + }, + limit: number; +} + function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } @@ -162,8 +192,7 @@ function decodeName(name: unknown): string | null { /** * POST a `NegotiateRequest` to the provider's `/negotiate` endpoint and - * return the provider-signed terms bundle. Mirrors - * `console-ui/src/lib/storage.ts::negotiateTerms`. + * return the provider-signed terms bundle. */ export async function negotiateTerms( providerUrl: string, @@ -389,6 +418,53 @@ export class DriveClient { return providers; } + async queryMatchingProviders( + query: QueryMatchingProvidersParams['query'], + limit: QueryMatchingProvidersParams['limit'], + ): Promise { + const api = this.requireApi(); + const matches = await api.apis.StorageProviderApi.find_matching_providers({ + bytes_needed: query.bytesNeeded, + min_duration: query.minDuration, + max_price_per_byte: query.maxPricePerByte, + primary_only: query.primaryOnly, + }, limit); + + return matches.map((match) => { + const info = match.info; + const maxCapacity = BigInt(info.max_capacity ?? 0); + const committedBytes = BigInt(info.committed_bytes ?? 0); + const availableCapacity = + maxCapacity > committedBytes ? maxCapacity - committedBytes : 0n; + + return { + account: toSs58(match.account), + multiaddr: new TextDecoder().decode(info.multiaddr), + stake: BigInt(info.stake ?? 0), + availableCapacity, + maxCapacity, + committedBytes, + pricePerByte: BigInt(info.price_per_byte ?? 0), + minDuration: info.min_duration ?? 0, + maxDuration: info.max_duration ?? 0, + acceptingPrimary: info.accepting_primary ?? false, + replicaSyncPrice: + info.replica_sync_price != null ? BigInt(info.replica_sync_price) : undefined, + acceptingExtensions: info.accepting_extensions ?? false, + registeredAt: Number(info.registered_at ?? 0), + agreementsTotal: info.agreements_total ?? 0, + agreementsExtended: info.agreements_extended ?? 0, + agreementsNotExtended: info.agreements_not_extended ?? 0, + agreementsBurned: info.agreements_burned ?? 0, + challengesReceived: info.challenges_received ?? 0, + challengesFailed: info.challenges_failed ?? 0, + matchScore: match.match_score, + partialReason: match.partial_reason?.type ?? "", + }; + }).sort((a, b) => b.matchScore - a.matchScore); + } + + // ── Account ─────────────────────────────────────────────────────────────── async getBalance(address: string): Promise<{ free: bigint; reserved: bigint }> { diff --git a/user-interfaces/drive-ui/src/state/drive.state.ts b/user-interfaces/drive-ui/src/state/drive.state.ts index 8ccd124d..dee0d9f0 100644 --- a/user-interfaces/drive-ui/src/state/drive.state.ts +++ b/user-interfaces/drive-ui/src/state/drive.state.ts @@ -10,6 +10,8 @@ import { BehaviorSubject, combineLatest, distinctUntilChanged, Subscription } fr import { bind } from "@react-rxjs/core"; import { DriveClient, + MatchingProviders, + QueryMatchingProvidersParams, type AvailableProvider, type DriveInfo, type FsEntry, @@ -367,15 +369,18 @@ export async function retryCreation(id: string): Promise { return runChainSubmit(id, ctx); } -/** - * Walk on-chain provider state and return the list of registered - * providers. The picker dialog consumes this before negotiation. - */ export async function listAvailableProviders(): Promise { if (!client.hasApi()) return []; return client.listAvailableProviders(); } +const DEFAULT_PROVIDER_LIMIT = 10; +export async function queryMatchingProviders(query: QueryMatchingProvidersParams['query'], limit: QueryMatchingProvidersParams['limit'] = DEFAULT_PROVIDER_LIMIT): Promise { + if (!client.hasApi()) return []; + return client.queryMatchingProviders(query, limit); +} + + export async function deleteDrive(driveId: bigint): Promise { if (!client.hasApi() || !client.hasSigner()) return; await client.deleteDrive(driveId); diff --git a/user-interfaces/shared/test-helpers/src/buckets.ts b/user-interfaces/shared/test-helpers/src/buckets.ts index 29f4e70e..647f7e28 100644 --- a/user-interfaces/shared/test-helpers/src/buckets.ts +++ b/user-interfaces/shared/test-helpers/src/buckets.ts @@ -21,6 +21,7 @@ interface NegotiateRequest { duration: number; price_per_byte: number | bigint; replica_params: unknown | null; + bucket_id: bigint | null; } interface SignedTerms { @@ -32,6 +33,7 @@ interface SignedTerms { valid_until: number; nonce: number | bigint; replica_params: unknown | null; + bucket_id: bigint | null; }; signature: string; } @@ -99,6 +101,7 @@ function buildSignedTermsArgs(providerAccount: string, signed: SignedTerms) { nonce: BigInt(t.nonce), // eslint-disable-next-line @typescript-eslint/no-explicit-any replica_params: (t.replica_params ?? undefined) as any, + bucket_id: t.bucket_id ?? undefined, }; return { provider: providerAccount, terms, sig }; } @@ -139,6 +142,7 @@ export async function createBucketViaApi( duration: opts.duration ?? 10_000, price_per_byte: opts.pricePerByte ?? 0n, replica_params: null, + bucket_id: null, }); const result = await submitExtrinsic( @@ -237,6 +241,7 @@ export async function createDriveViaApi( duration: opts.storagePeriod ?? 10_000, price_per_byte: opts.pricePerByte ?? 0n, replica_params: null, + bucket_id: null, }); const nameBytes = opts.name ? Binary.fromText(opts.name) : undefined; diff --git a/user-interfaces/shared/test-helpers/src/fixtures.ts b/user-interfaces/shared/test-helpers/src/fixtures.ts index 8dd9b50e..b8419a6e 100644 --- a/user-interfaces/shared/test-helpers/src/fixtures.ts +++ b/user-interfaces/shared/test-helpers/src/fixtures.ts @@ -4,7 +4,7 @@ import { getApi, getBestBlockNumber, disconnect, type ParachainApi } from "./cha const PROVIDER_HEALTH_URL = process.env.PROVIDER_HEALTH_URL ?? "http://127.0.0.1:3333/health"; -const DEFAULT_MIN_BLOCK_TIMEOUT = process.env.CI ? 60_000 : 60_000; +const DEFAULT_MIN_BLOCK_TIMEOUT = 60_000; export async function waitForConnection(page: Page, timeout = 30_000): Promise { await expect(page.getByTestId("block-number")).toBeVisible({ timeout }); diff --git a/user-interfaces/shared/test-helpers/src/setupProvider.ts b/user-interfaces/shared/test-helpers/src/setupProvider.ts new file mode 100644 index 00000000..55f517f8 --- /dev/null +++ b/user-interfaces/shared/test-helpers/src/setupProvider.ts @@ -0,0 +1,19 @@ + +import { + Alice, + cleanProviderRegistry, + registerProviderViaApi, + disconnectApi, +} from "@web3-storage/test-helpers"; + + +async function globalSetupProvider() { + await cleanProviderRegistry([Alice]); + await registerProviderViaApi(Alice); + disconnectApi(); +} + +globalSetupProvider().catch((err) => { + console.error("Failed to set up provider with error:", err); + process.exit(1); +}); \ No newline at end of file