From def51a1076aced9e33857be0d2970d200c1b5c63 Mon Sep 17 00:00:00 2001 From: Konrad Stepniak Date: Wed, 19 Jun 2024 14:28:41 +0200 Subject: [PATCH] test(pallet-market): unit tests publish_storage_deals --- Cargo.lock | 29 ++++++- Cargo.toml | 3 +- pallets/market/Cargo.toml | 4 +- pallets/market/src/lib.rs | 159 ++++++++++++++++++----------------- pallets/market/src/mock.rs | 75 +++++++++++++++-- pallets/market/src/test.rs | 165 +++++++++++++++++++++++++++++++------ 6 files changed, 322 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c66c17ad..f3c2b534a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3266,6 +3266,16 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -3279,6 +3289,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "environmental" version = "1.1.4" @@ -3481,7 +3504,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84f2e425d9790201ba4af4630191feac6dcc98765b118d4d18e91d23c2353866" dependencies = [ - "env_logger", + "env_logger 0.10.2", "log", ] @@ -7147,12 +7170,14 @@ dependencies = [ name = "pallet-market" version = "0.0.0" dependencies = [ - "bs58 0.5.1", + "blake2b_simd", "cid 0.11.1", + "env_logger 0.11.3", "frame-benchmarking", "frame-support", "frame-system", "log", + "multihash-codetable", "pallet-balances", "parity-scale-codec", "scale-info", diff --git a/Cargo.toml b/Cargo.toml index bf7bb5d41..dc625794f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ async-channel = "2.3.1" async-stream = "0.3.5" base64 = "0.22.1" bitflags = "2.5.0" -bs58 = { version = "0.5.1", default-features = false } +blake2b_simd = { version = "1.0.2" } byteorder = "1.5.0" bytes = "1.6.0" chrono = "0.4.38" @@ -53,6 +53,7 @@ ipld-dagpb = "0.2.1" itertools = "0.13.0" jsonrpsee = { version = "0.22.5" } log = { version = "0.4.21", default-features = false } +multihash-codetable = { version = "0.1.1", default-features = false } polkavm = "0.9.3" polkavm-derive = "0.9.1" polkavm-linker = "0.9.2" diff --git a/pallets/market/Cargo.toml b/pallets/market/Cargo.toml index 18fdb9e7a..127130c83 100644 --- a/pallets/market/Cargo.toml +++ b/pallets/market/Cargo.toml @@ -16,7 +16,6 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] -bs58 = { workspace = true, default-features = false } cid = { workspace = true, default-features = false, features = ["scale-codec"] } codec = { workspace = true, default-features = false, features = ["derive"] } log = { workspace = true } @@ -30,6 +29,9 @@ sp-arithmetic = { workspace = true, default-features = false } sp-std = { workspace = true, default-features = false } [dev-dependencies] +blake2b_simd = { workspace = true } +env_logger = { workspace = true } +multihash-codetable = { workspace = true, features = ["blake2b"] } pallet-balances = { workspace = true, default-features = false } sp-core = { workspace = true, default-features = false } sp-io = { workspace = true } diff --git a/pallets/market/src/lib.rs b/pallets/market/src/lib.rs index 89a215e55..cc85d1ff5 100644 --- a/pallets/market/src/lib.rs +++ b/pallets/market/src/lib.rs @@ -21,7 +21,7 @@ pub mod pallet { pub const CID_CODEC: u64 = 0x55; pub const LOG_TARGET: &'static str = "runtime::market"; - use cid::{multihash::Multihash, Cid}; + use cid::Cid; use codec::{Decode, Encode}; use frame_support::{ dispatch::DispatchResult, @@ -73,6 +73,7 @@ pub mod pallet { /// Must identify as an on-chain `Self::AccountId`. type OffchainPublic: IdentifyAccount; + /// How many deals can be published in a single batch of `publish_storage_deals`. #[pallet::constant] type MaxDeals: Get; @@ -140,39 +141,40 @@ pub mod pallet { // It cannot be generic over because, #[derive(RuntimeDebug, TypeInfo)] also make `T` to have `RuntimeDebug`/`TypeInfo` // It is a known rust issue pub struct DealProposal { + /// Byte Encoded Cid // We use BoundedVec here, as cid::Cid do not implement `TypeInfo`, so it cannot be saved into the Runtime Storage. // It maybe doable using newtype pattern, however not sure how the UI on the frontend side would handle that anyways. // There is Encode/Decode implementation though, through the feature flag: `scale-codec`. - piece_cid: BoundedVec>, - piece_size: u64, + pub piece_cid: BoundedVec>, + pub piece_size: u64, /// Storage Client's Account Id - client: Address, + pub client: Address, /// Storage Provider's Account Id - provider: Address, + pub provider: Address, /// Arbitrary client chosen label to apply to the deal - label: BoundedVec>, + pub label: BoundedVec>, /// Nominal start block. Deal payment is linear between StartBlock and EndBlock, /// with total amount StoragePricePerBlock * (EndBlock - StartBlock). /// Storage deal must appear in a sealed (proven) sector no later than StartBlock, /// otherwise it is invalid. - start_block: BlockNumber, + pub start_block: BlockNumber, /// When the Deal is supposed to end. - end_block: BlockNumber, + pub end_block: BlockNumber, /// `Deal` can be terminated early, by `on_sectors_terminate`. /// Before that, a Storage Provider can payout it's earned fees by calling `on_settle_deal_payments`. /// `on_settle_deal_payments` must know how much money it can payout, so it's related to the number of blocks (time) it was stored. /// Reference - storage_price_per_block: Balance, + pub storage_price_per_block: Balance, /// Amount of Balance (DOTs) Storage Provider stakes as Collateral for storing given `piece_cid` /// There should be enough Balance added by `add_balance` by Storage Provider to cover it. /// When the Deal fails/is terminated to early, this is the amount which get slashed. - provider_collateral: Balance, + pub provider_collateral: Balance, /// Current [`DealState`]. /// It goes: `Unpublished` -> `Published` -> `Active` - state: DealState, + pub state: DealState, } impl @@ -193,14 +195,8 @@ pub mod pallet { } fn cid(&self) -> Result { - let mh_bytes = bs58::decode(&self.piece_cid) - .into_vec() - .map_err(|e| ProposalError::Base58Error(e))?; - let cid = Cid::new_v1( - CID_CODEC, - Multihash::from_bytes(&mh_bytes).map_err(|e| ProposalError::InvalidMultihash(e))?, - ); - + let cid = + Cid::try_from(&self.piece_cid[..]).map_err(|e| ProposalError::InvalidCid(e))?; Ok(cid) } } @@ -247,6 +243,7 @@ pub mod pallet { who: T::AccountId, amount: BalanceOf, }, + /// Deal has been successfully published between a client and a provider. DealPublished { deal_id: DealId, client: T::AccountId, @@ -256,29 +253,39 @@ pub mod pallet { #[pallet::error] pub enum Error { - /// When a Market Participant tries to withdraw more + /// Market Participant tries to withdraw more /// funds than they have available on the Market, because: /// - they never deposited the amount they want to withdraw /// - the funds they deposited were locked as part of a deal InsufficientFreeFunds, + /// `publish_storage_deals` was called with empty `deals` array. NoProposalsToBePublished, + /// `publish_storage_deals` must be called by Storage Providers and it's a Provider of all of the deals. ProposalsNotPublishedByStorageProvider, + /// `publish_storage_deals` call was supplied with `deals` which are all invalid. AllProposalsInvalid, - NoValidProposals, - WrongSignature, + /// `publish_storage_deals`'s core logic was invoked with a broken invariant that should be called by `validate_deals`. + UnexpectedValidationError, } // NOTE(@th7nder,18/06/2024): // would love to use `thiserror` but it's not supporting no_std environments yet // `thiserror-core` relies on rust nightly feature: error_in_core + /// Errors related to [`DealProposal`] and [`ClientDealProposal`] + /// This is error does not surface externally, only in the logs. + /// Mostly used for Deal Validation [`Self::::validate_deals`]. #[derive(RuntimeDebug)] pub enum ProposalError { + /// ClientDealProposal.client_signature did not match client's public key and data. WrongSignature, + /// Deal's block_start > block_end, so it doesn't make sense. EndBeforeStart, + /// Deal has to be [`DealState::Unpublished`] when being Published NotUnpublished, + /// Deal's duration must be within `Config::MinDealDuration` < `Config:MaxDealDuration`. DurationOutOfBounds, - Base58Error(bs58::decode::Error), - InvalidMultihash(cid::multihash::Error), + /// Deal's piece_cid is invalid. + InvalidCid(cid::Error), } /// Extrinsics exposed by the pallet @@ -332,6 +339,11 @@ pub mod pallet { } /// Publish a new set of storage deals (not yet included in a sector). + /// It saves valid deals as [`DealState::Published`] and locks up client fees and provider's collaterals. + /// Locked up balances cannot be withdrawn until a deal is terminated. + /// All of the deals must belong to a single Storage Provider. + /// It is permissive, if some of the deals are correct and some are not, it emits events for valid deals. + /// On success emits [`Event::::DealPublished`] for each successful deal. pub fn publish_storage_deals( origin: OriginFor, deals: BoundedVec< @@ -344,24 +356,18 @@ pub mod pallet { T::MaxDeals, >, ) -> DispatchResult { - // TODO(@th7nder,19/06/2024): - // - pending proposal dedpulication - // - struct DealId(u64) - // - unit tests - // - docs - // - testing on substrate let provider = ensure_signed(origin)?; let (valid_deals, total_provider_lockup) = Self::validate_deals(provider.clone(), deals)?; // Lock up funds for the clients and emit events for mut deal in valid_deals.into_iter() { + // It should have been validated by validate_deals by now. let client_fee: BalanceOf = deal .total_storage_fee() - .expect("should have been validated by now in validate_deals") + .ok_or(Error::::UnexpectedValidationError)? .try_into() - .ok() - .expect("should have been validated by now in validate_deals"); + .map_err(|_| Error::::UnexpectedValidationError)?; BalanceTable::::try_mutate(&deal.client, |balance| -> DispatchResult { balance.free = balance @@ -414,6 +420,38 @@ pub mod pallet { T::PalletId::get().into_account_truncating() } + /// Validates the signature of the given data with the provided signer's account ID. + /// + /// # Errors + /// + /// This function returns a [`WrongSignature`](crate::Error::WrongSignature) error if the + /// signature is invalid or the verification process fails. + pub fn validate_signature( + data: &Vec, + signature: &T::OffchainSignature, + signer: &T::AccountId, + ) -> Result<(), ProposalError> { + if signature.verify(&**data, &signer) { + return Ok(()); + } + + // NOTE: for security reasons modern UIs implicitly wrap the data requested to sign into + // , that's why we support both wrapped and raw versions. + let prefix = b""; + let suffix = b""; + let mut wrapped = Vec::with_capacity(data.len() + prefix.len() + suffix.len()); + wrapped.extend(prefix); + wrapped.extend(data); + wrapped.extend(suffix); + + ensure!( + signature.verify(&*wrapped, &signer), + ProposalError::WrongSignature + ); + + Ok(()) + } + fn generate_deal_id() -> DealId { let ret = NextDealId::::get(); let next = ret @@ -430,12 +468,11 @@ pub mod pallet { BlockNumberFor, T::OffchainSignature, >, - provider: &T::AccountId, ) -> Result<(), ProposalError> { Self::validate_signature( &Encode::encode(&deal.proposal), &deal.client_signature, - &provider, + &deal.proposal.client, )?; // Ensure the Piece's Cid is parsable and valid @@ -500,9 +537,9 @@ pub mod pallet { let mut message_proposals: BoundedBTreeSet = BoundedBTreeSet::new(); - let valid_deals = deals.into_iter().filter_map(|deal| { - if let Err(e) = Self::sanity_check(&deal, &provider) { - log::info!(target: LOG_TARGET, "insane deal: {:?}, error: {:?}", deal, e); + let valid_deals = deals.into_iter().enumerate().filter_map(|(idx, deal)| { + if let Err(e) = Self::sanity_check(&deal) { + log::error!(target: LOG_TARGET, "insane deal: idx {}, error: {:?}", idx, e); return None; } @@ -518,8 +555,8 @@ pub mod pallet { let client_balance = BalanceTable::::get(&deal.proposal.client); if client_lockup > client_balance.free { - log::info!(target: LOG_TARGET, "invalid deal: client {:?} not enough free balance {:?} < {:?} to cover deal {:?}", - deal.proposal.client, client_balance.free, client_lockup, deal); + log::error!(target: LOG_TARGET, "invalid deal: client {:?} not enough free balance {:?} < {:?} to cover deal idx: {}", + deal.proposal.client, client_balance.free, client_lockup, idx); return None; } @@ -528,8 +565,8 @@ pub mod pallet { let provider_balance = BalanceTable::::get(&deal.proposal.provider); if provider_lockup > provider_balance.free { - log::info!(target: LOG_TARGET, "invalid deal: storage provider {:?} not enough free balance {:?} < {:?} to cover deal {:?}", - deal.proposal.provider, provider_balance.free, provider_lockup, deal); + log::error!(target: LOG_TARGET, "invalid deal: storage provider {:?} not enough free balance {:?} < {:?} to cover deal idx: {}", + deal.proposal.provider, provider_balance.free, provider_lockup, idx); return None; } @@ -537,10 +574,10 @@ pub mod pallet { let duplicate_in_state = PendingProposals::::get().contains(&hash); let duplicate_in_message = message_proposals.contains(&hash); if duplicate_in_state || duplicate_in_message { - log::info!(target: LOG_TARGET, "invalid deal: cannot publish duplicate deal: {:?}", deal); + log::error!(target: LOG_TARGET, "invalid deal: cannot publish duplicate deal idx: {}", idx); return None; } - PendingProposals::::get().try_insert(hash.clone()).ok()?; + PendingProposals::::get().try_insert(hash).ok()?; message_proposals.try_insert(hash).ok()?; // SAFETY: it'll always succeed, as there cannot be more clients than T::MaxDeals @@ -557,7 +594,7 @@ pub mod pallet { // Used for deduplication purposes // We don't want to store another BTreeSet of ClientDealProposals // We only care about hashes. - // It is not an associated function, because T::Hashing was hard to use inside of there. + // It is not an associated function, because T::Hashing is hard to use inside of there. fn hash_proposal( proposal: &ClientDealProposal< T::AccountId, @@ -569,37 +606,5 @@ pub mod pallet { let bytes = Encode::encode(proposal); T::Hashing::hash(&bytes) } - - /// Validates the signature of the given data with the provided signer's account ID. - /// - /// # Errors - /// - /// This function returns a [`WrongSignature`](crate::Error::WrongSignature) error if the - /// signature is invalid or the verification process fails. - pub fn validate_signature( - data: &Vec, - signature: &T::OffchainSignature, - signer: &T::AccountId, - ) -> Result<(), ProposalError> { - if signature.verify(&**data, &signer) { - return Ok(()); - } - - // NOTE: for security reasons modern UIs implicitly wrap the data requested to sign into - // , that's why we support both wrapped and raw versions. - let prefix = b""; - let suffix = b""; - let mut wrapped = Vec::with_capacity(data.len() + prefix.len() + suffix.len()); - wrapped.extend(prefix); - wrapped.extend(data); - wrapped.extend(suffix); - - ensure!( - signature.verify(&*wrapped, &signer), - ProposalError::WrongSignature - ); - - Ok(()) - } } } diff --git a/pallets/market/src/mock.rs b/pallets/market/src/mock.rs index a9150c8ea..92bec1f13 100644 --- a/pallets/market/src/mock.rs +++ b/pallets/market/src/mock.rs @@ -1,8 +1,15 @@ +use cid::Cid; +use codec::Encode; use frame_support::{derive_impl, parameter_types, PalletId}; -use frame_system as system; -use sp_runtime::BuildStorage; +use frame_system::{self as system, pallet_prelude::BlockNumberFor}; +use multihash_codetable::{Code, MultihashDigest}; +use sp_core::Pair; +use sp_runtime::{ + traits::{ConstU32, ConstU64, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, MultiSignature, MultiSigner, +}; -use crate as pallet_market; +use crate::{self as pallet_market, BalanceOf, ClientDealProposal, DealProposal, CID_CODEC}; type Block = frame_system::mocking::MockBlock; @@ -16,10 +23,16 @@ frame_support::construct_runtime!( } ); +pub type Signature = MultiSignature; +pub type AccountPublic = ::Signer; +pub type AccountId = ::AccountId; + #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl system::Config for Test { type Block = Block; type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type Lookup = IdentityLookup; } #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] @@ -35,10 +48,56 @@ impl crate::Config for Test { type RuntimeEvent = RuntimeEvent; type PalletId = MarketPalletId; type Currency = Balances; + type OffchainSignature = Signature; + type OffchainPublic = AccountPublic; + type MaxDeals = ConstU32<32>; + type BlocksPerDay = ConstU64<1>; + type MinDealDuration = ConstU64<1>; + type MaxDealDuration = ConstU64<30>; +} + +pub type AccountIdOf = ::AccountId; + +pub fn key_pair(name: &str) -> sp_core::sr25519::Pair { + sp_core::sr25519::Pair::from_string(name, None).unwrap() +} + +pub fn account(name: &str) -> AccountIdOf { + let user_pair = key_pair(name); + let signer = MultiSigner::Sr25519(user_pair.public()); + signer.into_account() +} + +pub fn sign(pair: &sp_core::sr25519::Pair, bytes: &[u8]) -> MultiSignature { + MultiSignature::Sr25519(pair.sign(bytes)) +} + +pub fn cid_of(data: &str) -> cid::Cid { + Cid::new_v1(CID_CODEC, Code::Blake2b256.digest(data.as_bytes())) +} + +type DealProposalOf = + DealProposal<::AccountId, BalanceOf, BlockNumberFor>; + +type ClientDealProposalOf = ClientDealProposal< + ::AccountId, + BalanceOf, + BlockNumberFor, + MultiSignature, +>; + +pub fn sign_proposal(client: &str, proposal: DealProposalOf) -> ClientDealProposalOf { + let alice_pair = key_pair(client); + let client_signature = sign(&alice_pair, &Encode::encode(&proposal)); + ClientDealProposal { + proposal, + client_signature, + } } -pub const ALICE: u64 = 0; -pub const BOB: u64 = 1; +pub const ALICE: &'static str = "//Alice"; +pub const BOB: &'static str = "//Bob"; +pub const PROVIDER: &'static str = "//StorageProvider"; pub const INITIAL_FUNDS: u64 = 100; /// Build genesis storage according to the mock runtime. @@ -48,7 +107,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .unwrap() .into(); pallet_balances::GenesisConfig:: { - balances: vec![(ALICE, INITIAL_FUNDS), (BOB, INITIAL_FUNDS)], + balances: vec![ + (account(ALICE), INITIAL_FUNDS), + (account(BOB), INITIAL_FUNDS), + (account(PROVIDER), INITIAL_FUNDS), + ], } .assimilate_storage(&mut t) .unwrap(); diff --git a/pallets/market/src/test.rs b/pallets/market/src/test.rs index b6c8fde9c..5d33eefad 100644 --- a/pallets/market/src/test.rs +++ b/pallets/market/src/test.rs @@ -1,30 +1,33 @@ use frame_support::{ assert_noop, assert_ok, - sp_runtime::{ArithmeticError, TokenError}, + sp_runtime::{bounded_vec, ArithmeticError, TokenError}, }; -use crate::{mock::*, BalanceEntry, BalanceTable, Error, Event}; +use crate::{mock::*, BalanceEntry, BalanceTable, DealProposal, DealState, Error, Event}; #[test] fn initial_state() { new_test_ext().execute_with(|| { assert_eq!(Balances::free_balance(Market::account_id()), 0); assert_eq!( - BalanceTable::::get(ALICE), + BalanceTable::::get(account(ALICE)), BalanceEntry:: { free: 0, locked: 0 } ); }); } #[test] -fn basic_end_to_end_works() { +fn adds_and_withdraws_balances() { new_test_ext().execute_with(|| { // Adds funds from an account to the Market - assert_ok!(Market::add_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_ok!(Market::add_balance( + RuntimeOrigin::signed(account(ALICE)), + 10 + )); assert_eq!(Balances::free_balance(Market::account_id()), 10); - assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS - 10); + assert_eq!(Balances::free_balance(account(ALICE)), INITIAL_FUNDS - 10); assert_eq!( - BalanceTable::::get(ALICE), + BalanceTable::::get(account(ALICE)), BalanceEntry:: { free: 10, locked: 0, @@ -32,11 +35,14 @@ fn basic_end_to_end_works() { ); // Is able to withdraw added funds back - assert_ok!(Market::withdraw_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_ok!(Market::withdraw_balance( + RuntimeOrigin::signed(account(ALICE)), + 10 + )); assert_eq!(Balances::free_balance(Market::account_id()), 0); - assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS); + assert_eq!(Balances::free_balance(account(ALICE)), INITIAL_FUNDS); assert_eq!( - BalanceTable::::get(ALICE), + BalanceTable::::get(account(ALICE)), BalanceEntry:: { free: 0, locked: 0 } ); }); @@ -45,11 +51,14 @@ fn basic_end_to_end_works() { #[test] fn adds_balance() { new_test_ext().execute_with(|| { - assert_ok!(Market::add_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_ok!(Market::add_balance( + RuntimeOrigin::signed(account(ALICE)), + 10 + )); assert_eq!(Balances::free_balance(Market::account_id()), 10); - assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS - 10); + assert_eq!(Balances::free_balance(account(ALICE)), INITIAL_FUNDS - 10); assert_eq!( - BalanceTable::::get(ALICE), + BalanceTable::::get(account(ALICE)), BalanceEntry:: { free: 10, locked: 0, @@ -67,12 +76,12 @@ fn adds_balance() { free_balance: 10 }), RuntimeEvent::Balances(pallet_balances::Event::::Transfer { - from: ALICE, + from: account(ALICE), to: Market::account_id(), amount: 10 }), RuntimeEvent::Market(Event::::BalanceAdded { - who: ALICE, + who: account(ALICE), amount: 10 }) ] @@ -80,7 +89,7 @@ fn adds_balance() { // Makes sure other accounts are unaffected assert_eq!( - BalanceTable::::get(BOB), + BalanceTable::::get(account(BOB)), BalanceEntry:: { free: 0, locked: 0 } ); }); @@ -90,7 +99,7 @@ fn adds_balance() { fn fails_to_add_balance_insufficient_funds() { new_test_ext().execute_with(|| { assert_noop!( - Market::add_balance(RuntimeOrigin::signed(ALICE), INITIAL_FUNDS + 1), + Market::add_balance(RuntimeOrigin::signed(account(ALICE)), INITIAL_FUNDS + 1), TokenError::FundsUnavailable, ); }); @@ -101,7 +110,7 @@ fn fails_to_add_balance_overflow() { new_test_ext().execute_with(|| { // Hard to do this without setting it explicitly in the map BalanceTable::::set( - BOB, + account(BOB), BalanceEntry:: { free: u64::MAX, locked: 0, @@ -109,7 +118,7 @@ fn fails_to_add_balance_overflow() { ); assert_noop!( - Market::add_balance(RuntimeOrigin::signed(BOB), 1), + Market::add_balance(RuntimeOrigin::signed(account(BOB)), 1), ArithmeticError::Overflow ); }); @@ -118,14 +127,17 @@ fn fails_to_add_balance_overflow() { #[test] fn withdraws_balance() { new_test_ext().execute_with(|| { - let _ = Market::add_balance(RuntimeOrigin::signed(ALICE), 10); + let _ = Market::add_balance(RuntimeOrigin::signed(account(ALICE)), 10); System::reset_events(); - assert_ok!(Market::withdraw_balance(RuntimeOrigin::signed(ALICE), 10)); + assert_ok!(Market::withdraw_balance( + RuntimeOrigin::signed(account(ALICE)), + 10 + )); assert_eq!(Balances::free_balance(Market::account_id()), 0); - assert_eq!(Balances::free_balance(ALICE), INITIAL_FUNDS); + assert_eq!(Balances::free_balance(account(ALICE)), INITIAL_FUNDS); assert_eq!( - BalanceTable::::get(ALICE), + BalanceTable::::get(account(ALICE)), BalanceEntry:: { free: 0, locked: 0 } ); @@ -137,11 +149,11 @@ fn withdraws_balance() { }), RuntimeEvent::Balances(pallet_balances::Event::::Transfer { from: Market::account_id(), - to: ALICE, + to: account(ALICE), amount: 10 }), RuntimeEvent::Market(Event::::BalanceWithdrawn { - who: ALICE, + who: account(ALICE), amount: 10 }) ] @@ -153,10 +165,111 @@ fn withdraws_balance() { fn fails_to_withdraw_balance() { new_test_ext().execute_with(|| { assert_noop!( - Market::withdraw_balance(RuntimeOrigin::signed(BOB), 10), + Market::withdraw_balance(RuntimeOrigin::signed(account(BOB)), 10), Error::::InsufficientFreeFunds ); assert_eq!(events(), []); }); } + +#[test] +fn publish_storage_deals_fails_with_empty_deals() { + new_test_ext().execute_with(|| { + assert_noop!( + Market::publish_storage_deals(RuntimeOrigin::signed(account(PROVIDER)), bounded_vec![]), + Error::::NoProposalsToBePublished + ); + }); +} + +#[test] +fn publish_storage_deals() { + let _ = env_logger::try_init(); + + new_test_ext().execute_with(|| { + let alice_proposal = sign_proposal( + ALICE, + DealProposal { + piece_cid: cid_of("polka-storage-data") + .to_bytes() + .try_into() + .expect("hash is always 32 bytes"), + piece_size: 18, + client: account(ALICE), + provider: account(PROVIDER), + label: bounded_vec![0xb, 0xe, 0xe, 0xf], + start_block: 100, + end_block: 110, + storage_price_per_block: 5, + provider_collateral: 25, + state: DealState::Unpublished, + }, + ); + let bob_proposal = sign_proposal( + BOB, + DealProposal { + piece_cid: cid_of("polka-storage-data-bob") + .to_bytes() + .try_into() + .expect("hash is always 32 bytes"), + piece_size: 21, + client: account(BOB), + provider: account(PROVIDER), + label: bounded_vec![0xa, 0xe, 0xe, 0xf], + start_block: 130, + end_block: 135, + storage_price_per_block: 10, + provider_collateral: 15, + state: DealState::Unpublished, + }, + ); + + let _ = Market::add_balance(RuntimeOrigin::signed(account(ALICE)), 60); + let _ = Market::add_balance(RuntimeOrigin::signed(account(BOB)), 70); + let _ = Market::add_balance(RuntimeOrigin::signed(account(PROVIDER)), 75); + System::reset_events(); + + assert_ok!(Market::publish_storage_deals( + RuntimeOrigin::signed(account(PROVIDER)), + bounded_vec![alice_proposal, bob_proposal] + )); + assert_eq!( + BalanceTable::::get(account(ALICE)), + BalanceEntry:: { + free: 10, + locked: 50 + } + ); + assert_eq!( + BalanceTable::::get(account(BOB)), + BalanceEntry:: { + free: 20, + locked: 50 + } + ); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 35, + locked: 40 + } + ); + + assert_eq!( + events(), + [ + RuntimeEvent::Market(Event::::DealPublished { + deal_id: 0, + client: account(ALICE), + provider: account(PROVIDER), + }), + RuntimeEvent::Market(Event::::DealPublished { + deal_id: 1, + client: account(BOB), + provider: account(PROVIDER), + }), + ] + ); + }); +}