From b9a08ebf63ee72a5a5a2c8e406a5d6738d39b33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Duarte?= Date: Wed, 26 Jun 2024 16:59:20 +0100 Subject: [PATCH] feat(pallet-market): implement on_sectors_terminate --- pallets/market/src/lib.rs | 357 ++++++++++++++++++++++++++---- pallets/market/src/test.rs | 377 +++++++++++++++++++++++++++++++- primitives/proofs/src/traits.rs | 19 +- 3 files changed, 703 insertions(+), 50 deletions(-) diff --git a/pallets/market/src/lib.rs b/pallets/market/src/lib.rs index 7e43c8ff2..4dfb873f7 100644 --- a/pallets/market/src/lib.rs +++ b/pallets/market/src/lib.rs @@ -18,9 +18,6 @@ mod test; // TODO(@th7nder,#77,14/06/2024): take the pallet out of dev mode #[frame_support::pallet(dev_mode)] pub mod pallet { - pub const CID_CODEC: u64 = 0x55; - pub const LOG_TARGET: &'static str = "runtime::market"; - use cid::Cid; use codec::{Decode, Encode}; use frame_support::{ @@ -32,6 +29,7 @@ pub mod pallet { ArithmeticError, BoundedBTreeMap, RuntimeDebug, }, traits::{ + tokens::WithdrawReasons, Currency, ExistenceRequirement::{AllowDeath, KeepAlive}, ReservableCurrency, @@ -41,13 +39,17 @@ pub mod pallet { use frame_system::{pallet_prelude::*, Config as SystemConfig, Pallet as System}; use multihash_codetable::{Code, MultihashDigest}; use primitives_proofs::{ - ActiveDeal, ActiveSector, DealId, Market, RegisteredSealProof, SectorDeal, SectorNumber, - SectorSize, MAX_DEALS_FOR_ALL_SECTORS, MAX_DEALS_PER_SECTOR, MAX_SECTORS_PER_CALL, + ActiveDeal, ActiveSector, DealId, Market, RegisteredSealProof, SectorDeal, SectorId, + SectorNumber, SectorSize, MAX_DEALS_FOR_ALL_SECTORS, MAX_DEALS_PER_SECTOR, + MAX_SECTORS_PER_CALL, }; use scale_info::TypeInfo; use sp_arithmetic::traits::BaseArithmetic; use sp_std::vec::Vec; + pub const CID_CODEC: u64 = 0x55; + pub const LOG_TARGET: &'static str = "runtime::market"; + /// Allows to extract Balance of an account via the Config::Currency associated type. /// BalanceOf is a sophisticated way of getting an u128. pub type BalanceOf = @@ -186,7 +188,7 @@ pub mod pallet { } impl ActiveDealState { - fn new( + pub(crate) fn new( sector_number: SectorNumber, sector_start_block: BlockNumber, ) -> ActiveDealState { @@ -296,6 +298,11 @@ pub mod pallet { pub type PendingProposals = StorageValue<_, BoundedBTreeSet, ValueQuery>; + /// Holds a mapping from [`SectorId`] to its respective [`DealId`]s. + #[pallet::storage] + pub type SectorDeals = + StorageMap<_, _, SectorId, BoundedVec>>; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -330,6 +337,18 @@ pub mod pallet { }, /// Deal was slashed. DealSlashed(DealId), + + /// Deal has been terminated. + /// + /// A deal may be voluntarily terminated by the storage provider, + /// or involuntarily, if the sector has been faulty for 42 consecutive days. + /// + /// Source: + DealTerminated { + deal_id: DealId, + client: T::AccountId, + provider: T::AccountId, + }, } /// Utility type to ensure that the bound for deal settlement is in sync. @@ -415,6 +434,26 @@ pub mod pallet { ExpiredDeal, } + #[derive(RuntimeDebug)] + pub enum SectorTerminateError { + /// Deal was not found in the [`Proposals`] table. + DealNotFound, + /// Caller is not the provider. + InvalidCaller, + /// Deal is not active + DealIsNotActive, + } + + impl From for DispatchError { + fn from(value: SectorTerminateError) -> Self { + DispatchError::Other(match value { + SectorTerminateError::DealNotFound => "deal was not found", + SectorTerminateError::InvalidCaller => "caller is not the provider", + SectorTerminateError::DealIsNotActive => "sector contains active deals", + }) + } + } + /// Extrinsics exposed by the pallet #[pallet::call] impl Pallet { @@ -431,13 +470,14 @@ pub mod pallet { .ok_or(ArithmeticError::Overflow)?; T::Currency::transfer(&caller, &Self::account_id(), amount, KeepAlive)?; - Self::deposit_event(Event::::BalanceAdded { - who: caller.clone(), - amount, - }); Ok(()) })?; + Self::deposit_event(Event::::BalanceAdded { + who: caller.clone(), + amount, + }); + Ok(()) } @@ -455,13 +495,14 @@ pub mod pallet { // The Market Pallet account will be reaped if no one is participating in the market. T::Currency::transfer(&Self::account_id(), &caller, amount, AllowDeath)?; - Self::deposit_event(Event::::BalanceWithdrawn { - who: caller.clone(), - amount, - }); Ok(()) })?; + Self::deposit_event(Event::::BalanceWithdrawn { + who: caller.clone(), + amount, + }); + Ok(()) } @@ -637,19 +678,8 @@ pub mod pallet { .try_into() .map_err(|_| Error::::UnexpectedValidationError)?; - BalanceTable::::try_mutate(&deal.client, |balance| -> DispatchResult { - // PRE-COND: always succeeds, validated by `validate_deals` - balance.free = balance - .free - .checked_sub(&client_fee) - .ok_or(ArithmeticError::Underflow)?; - balance.locked = balance - .locked - .checked_add(&client_fee) - .ok_or(ArithmeticError::Overflow)?; - - Ok(()) - })?; + // PRE-COND: always succeeds, validated by `validate_deals` + lock_funds::(&deal.client, client_fee)?; deal.state = DealState::Published; let deal_id = Self::generate_deal_id(); @@ -664,17 +694,7 @@ pub mod pallet { // Lock up funds for the Storage Provider // PRE-COND: always succeeds, validated by `validate_deals` - BalanceTable::::try_mutate(&provider, |balance| -> DispatchResult { - balance.free = balance - .free - .checked_sub(&total_provider_lockup) - .ok_or(ArithmeticError::Underflow)?; - balance.locked = balance - .locked - .checked_add(&total_provider_lockup) - .ok_or(ArithmeticError::Overflow)?; - Ok(()) - })?; + lock_funds::(&provider, total_provider_lockup)?; Ok(()) } @@ -1045,12 +1065,6 @@ pub mod pallet { Ok(unsealed_cids) } - /// Activate a set of deals grouped by sector, returning the size and - /// extra info about verified deals. - /// Sectors' deals are activated in parameter-defined order. - /// Each sector's deals are activated or fail as a group, but independently of other sectors. - /// Note that confirming all deals fit within a sector is the caller's responsibility - /// (and is implied by confirming the sector's data commitment is derived from the deal pieces). fn activate_deals( storage_provider: &T::AccountId, sector_deals: BoundedVec>, ConstU32>, @@ -1144,13 +1158,131 @@ pub mod pallet { Ok(activations) } + + /// Terminate a set of deals in response to their sector being terminated. + /// + /// Slashes the provider collateral, refunds the partial unpaid escrow amount to the client. + /// + /// A sector can be terminated voluntarily — the storage provider terminates the sector — + /// or involuntarily — the sector has been faulty for more than 42 consecutive days. + /// + /// Source: + fn on_sectors_terminate( + storage_provider: &T::AccountId, + sector_ids: BoundedVec>, + ) -> DispatchResult { + // TODO: check that the caller is actually a storage provider (?) + + // NOTE(@jmg-duarte,03/07/2024): the usage of the `current_block` NEEDS to be revised + // in the future as this function MAY be called on a different block than the current one. + // This is a consequence of the fact that this function is called indirectly, + // through a chain of calls that start on deferred cron events + let current_block = >::block_number(); + + for sector_id in sector_ids { + // In the original implementation, all sectors are popped, here, we take them all + let Some(deal_ids) = SectorDeals::::take(sector_id) else { + // Not found sectors are ignored, if we don't find any, we don't do anything + continue; + }; + + for deal_id in deal_ids { + // Fetch the corresponding deal proposal, it's ok if it has already been deleted + let Some(mut deal_proposal) = Proposals::::get(deal_id) else { + return Err(SectorTerminateError::DealNotFound)?; + }; + + if *storage_provider != deal_proposal.provider { + return Err(SectorTerminateError::InvalidCaller)?; + } + + if deal_proposal.end_block <= current_block { + // not slashing finished deals + continue; + } + + let hash_proposal = Self::hash_proposal(&deal_proposal); + // If a sector is being terminated, it means that at some point, + // the deals contained within were active + let DealState::Active(ref mut active_deal_state) = deal_proposal.state else { + return Err(SectorTerminateError::DealIsNotActive)?; + }; + + // https://github.com/filecoin-project/builtin-actors/blob/54236ae89880bf4aa89b0dba6d9060c3fd2aacee/actors/market/src/lib.rs#L840-L844 + if let Some(_) = active_deal_state.slash_block { + log::warn!("deal {} was already slashed, terminating anyway", deal_id); + } + + // https://github.com/filecoin-project/builtin-actors/blob/54236ae89880bf4aa89b0dba6d9060c3fd2aacee/actors/market/src/lib.rs#L846-L850 + if let None = active_deal_state.last_updated_block { + PendingProposals::::mutate(|pending_proposals| { + pending_proposals.remove(&hash_proposal); + }); + } + + // Handle payments + // https://github.com/filecoin-project/builtin-actors/blob/54236ae89880bf4aa89b0dba6d9060c3fd2aacee/actors/market/src/state.rs#L922-L962 + + // https://github.com/filecoin-project/builtin-actors/blob/17ede2b256bc819dc309edf38e031e246a516486/actors/market/src/state.rs#L932-L933 + let payment_start_block = calculate_start_block( + deal_proposal.start_block, + active_deal_state.last_updated_block, + ); + // The only reason we can use `current_block` is because of the line + // https://github.com/filecoin-project/builtin-actors/blob/54236ae89880bf4aa89b0dba6d9060c3fd2aacee/actors/market/src/lib.rs#L852 + let payment_end_block = + calculate_end_block(current_block, deal_proposal.end_block); + let n_blocks_elapsed = + calculate_elapsed_blocks(payment_start_block, payment_end_block); + + let total_payment = calculate_storage_price::( + n_blocks_elapsed, + deal_proposal.storage_price_per_block, + )?; + + // Pay any outstanding debts to the provider + perform_storage_payment::( + &deal_proposal.client, + &deal_proposal.provider, + total_payment, + )?; + // Slash and burn the provider collateral + slash_and_burn::( + &deal_proposal.provider, + deal_proposal.provider_collateral, + )?; + + // The remaining client locked funds should be counted from + // everything we just paid until the deal's end block + let remaining_client_collateral = calculate_storage_price::( + deal_proposal.end_block - payment_end_block, + deal_proposal.storage_price_per_block, + )?; + // We then unlock those client funds + unlock_funds::(&deal_proposal.client, remaining_client_collateral)?; + + // Remove completed deal + let _ = Proposals::::remove(deal_id); + + Self::deposit_event(Event::::DealTerminated { + deal_id, + client: deal_proposal.client.clone(), + provider: deal_proposal.provider.clone(), + }); + } + } + Ok(()) + } } + // NOTE(@jmg-duarte,01/07/2024): having free functions instead of implemented ones makes it harder + // to mistakenly make them public or interact weirdly with the Polkadot macros + /// Moves the provided `amount` from the `client`'s locked funds, to the provider's `free` funds. /// /// # Pre-Conditions /// * The client MUST have the necessary funds locked. - fn perform_storage_payment( + pub(crate) fn perform_storage_payment( client: &T::AccountId, provider: &T::AccountId, amount: BalanceOf, @@ -1176,4 +1308,139 @@ pub mod pallet { Ok(()) } + + /// Unlock a given `amount` of funds from the target account. + /// + /// Moves funds from `locked` to `free`. + #[inline(always)] + pub(crate) fn unlock_funds( + account_id: &T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + BalanceTable::::try_mutate(account_id, |balance| -> DispatchResult { + balance.locked = balance + .locked + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + + balance.free = balance + .free + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + + Ok(()) + }) + } + + /// Lock a given `amount` of funds from the target account. + /// + /// Moves funds from `free` to `locked`. + #[inline(always)] + pub(crate) fn lock_funds( + account_id: &T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + BalanceTable::::try_mutate(account_id, |balance| -> DispatchResult { + balance.free = balance + .free + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + + balance.locked = balance + .locked + .checked_add(&amount) + .ok_or(ArithmeticError::Overflow)?; + + Ok(()) + }) + } + + /// Slash and burn the provided `amount` from a given account. + /// + /// Sets `locked` to `locked - amount` and burns `amount`. + pub(crate) fn slash_and_burn( + account_id: &T::AccountId, + amount: BalanceOf, + ) -> DispatchResult { + BalanceTable::::try_mutate(account_id, |balance| -> DispatchResult { + let locked = balance + .locked + .checked_sub(&amount) + .ok_or(ArithmeticError::Underflow)?; + balance.locked = locked; + Ok(()) + })?; + // Burn from circulating supply + let imbalance = T::Currency::burn(amount); + // Remove burned amount from the market account + T::Currency::settle( + &T::PalletId::get().into_account_truncating(), + imbalance, + WithdrawReasons::FEE, + KeepAlive, + ) + // If we burned X, tried to settle X and failed, we're in a bad state + .map_err(|_| DispatchError::Corruption) + } + + // NOTE(@jmg-duarte,01/07/2024): calculate* functions to ease conversions and so on + + /// Calculate the start block. + /// + /// If `last_updated_block` is `None`, returns `start_block`. + /// Otherwise, returns the `max` between `start_block` and `last_updated_block`. + #[inline(always)] + fn calculate_start_block( + start_block: BlockNumber, + last_updated_block: Option, + ) -> BlockNumber { + if let Some(last_updated_block) = last_updated_block { + core::cmp::max(start_block, last_updated_block) + } else { + start_block + } + } + + /// Calculate the end block. + /// + /// Returns the `min` between the `current_block` and `end_block`. + #[inline(always)] + fn calculate_end_block( + current_block: BlockNumber, + end_block: BlockNumber, + ) -> BlockNumber { + core::cmp::min(current_block, end_block) + } + + /// Calculate the number of elapsed blocks. + /// + /// Returns the `max` between `end_block - start_block` and `0`. + #[inline(always)] + fn calculate_elapsed_blocks( + start_block: BlockNumber, + end_block: BlockNumber, + ) -> BlockNumber { + // https://github.com/filecoin-project/builtin-actors/blob/17ede2b256bc819dc309edf38e031e246a516486/actors/market/src/state.rs#L934-L935 + core::cmp::max(end_block - start_block, 0.into()) + } + + /// Calculate the storage price for a given `n_blocks` at a rate of `price_per_block`. + /// + /// Internally, this function converts both values to [`u128`], multiplies them, + /// and converts back to [`BalanceOf`], if at any point the conversion fails, + /// it is assumed to be an overflow and [`ArithmeticError::Overflow`] is returned. + #[inline(always)] + fn calculate_storage_price( + n_blocks: BlockNumberFor, + price_per_block: BalanceOf, + ) -> Result, ArithmeticError> + where + T: Config, + { + let n_blocks = + TryInto::::try_into(n_blocks).map_err(|_| ArithmeticError::Overflow)?; + let price_per_block = + TryInto::::try_into(price_per_block).map_err(|_| ArithmeticError::Overflow)?; + TryInto::try_into(price_per_block * n_blocks).map_err(|_| ArithmeticError::Overflow) + } } diff --git a/pallets/market/src/test.rs b/pallets/market/src/test.rs index bf08a0dd1..cb1ccbcbf 100644 --- a/pallets/market/src/test.rs +++ b/pallets/market/src/test.rs @@ -3,18 +3,22 @@ use core::str::FromStr; use cid::Cid; use frame_support::{ assert_err, assert_noop, assert_ok, + pallet_prelude::ConstU32, sp_runtime::{bounded_vec, ArithmeticError, DispatchError, TokenError}, - BoundedBTreeSet, + traits::Currency, + BoundedBTreeSet, BoundedVec, }; use primitives_proofs::{ ActiveDeal, ActiveSector, DealId, Market as MarketTrait, RegisteredSealProof, SectorDeal, + MAX_DEALS_PER_SECTOR, }; use crate::{ - mock::*, ActiveDealState, BalanceEntry, BalanceTable, DealProposal, DealSettlementError, - DealState, Error, Event, PendingProposals, Proposals, + mock::*, + pallet::{lock_funds, slash_and_burn, unlock_funds}, + ActiveDealState, BalanceEntry, BalanceTable, DealProposal, DealSettlementError, DealState, + Error, Event, PendingProposals, Proposals, SectorDeals, SectorTerminateError, }; - #[test] fn initial_state() { new_test_ext().execute_with(|| { @@ -867,3 +871,368 @@ fn settle_deal_payments_success_finished() { assert_eq!(Proposals::::get(0), None); }); } + +#[test] +fn test_lock_funds() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + assert_eq!( + ::Currency::total_balance(&account(PROVIDER)), + 100 + ); + // We can't get all 100, otherwise the account would be reaped + assert_ok!(Market::add_balance( + RuntimeOrigin::signed(account(PROVIDER)), + 90 + )); + assert_eq!( + ::Currency::total_balance(&account(PROVIDER)), + 10 + ); + assert_ok!(lock_funds::(&account(PROVIDER), 25)); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 65, + locked: 25, + } + ); + + assert_ok!(lock_funds::(&account(PROVIDER), 65)); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 0, + locked: 90, + } + ); + + assert_err!( + lock_funds::(&account(PROVIDER), 25), + DispatchError::Arithmetic(ArithmeticError::Underflow) + ); + + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 0, + locked: 90, + } + ); + }); +} + +#[test] +fn test_unlock_funds() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + assert_eq!( + ::Currency::total_balance(&account(PROVIDER)), + 100 + ); + // We can't get all 100, otherwise the account would be reaped + assert_ok!(Market::add_balance( + RuntimeOrigin::signed(account(PROVIDER)), + 90 + )); + assert_eq!( + ::Currency::total_balance(&account(PROVIDER)), + 10 + ); + assert_ok!(lock_funds::(&account(PROVIDER), 90)); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 0, + locked: 90, + } + ); + + assert_ok!(unlock_funds::(&account(PROVIDER), 30)); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 30, + locked: 60, + } + ); + + assert_ok!(unlock_funds::(&account(PROVIDER), 60)); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 90, + locked: 0, + } + ); + + assert_err!( + unlock_funds::(&account(PROVIDER), 60), + DispatchError::Arithmetic(ArithmeticError::Underflow) + ); + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 90, + locked: 0, + } + ); + }); +} + +#[test] +fn slash_and_burn_acc() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + assert_eq!( + ::Currency::total_issuance(), + 300 + ); + assert_ok!(Market::add_balance( + RuntimeOrigin::signed(account(PROVIDER)), + 75 + )); + + System::reset_events(); + + assert_ok!(lock_funds::(&account(PROVIDER), 10)); + assert_ok!(slash_and_burn::(&account(PROVIDER), 10)); + + assert_eq!( + events(), + [RuntimeEvent::Balances( + pallet_balances::Event::::Withdraw { + who: Market::account_id(), + amount: 10 + } + ),] + ); + assert_eq!( + ::Currency::total_issuance(), + 290 + ); + + assert_eq!( + BalanceTable::::get(account(PROVIDER)), + BalanceEntry:: { + free: 65, + locked: 0, + } + ); + + assert_err!( + slash_and_burn::(&account(PROVIDER), 10), + DispatchError::Arithmetic(ArithmeticError::Underflow) + ); + assert_eq!( + ::Currency::total_issuance(), + 290 + ); + }); +} + +#[test] +fn on_sector_terminate_unknown_deals() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + let _ = Market::add_balance(RuntimeOrigin::signed(account(PROVIDER)), 75); + System::reset_events(); + + let cid = BoundedVec::try_from(cid_of("polka_storage_cid").to_bytes()).unwrap(); + assert_ok!(Market::on_sectors_terminate( + &account(PROVIDER), + bounded_vec![cid], + )); + + assert_eq!(events(), []); + }); +} + +#[test] +fn on_sector_terminate_deal_not_found() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + let _ = Market::add_balance(RuntimeOrigin::signed(account(PROVIDER)), 75); + System::reset_events(); + + let cid = BoundedVec::try_from(cid_of("polka_storage_cid").to_bytes()).unwrap(); + let sector_deal_ids: BoundedVec<_, ConstU32> = bounded_vec![1]; + + SectorDeals::::insert(cid.clone(), sector_deal_ids); + + assert_err!( + Market::on_sectors_terminate( + &account(PROVIDER), + bounded_vec![cid], + ), + DispatchError::from(SectorTerminateError::DealNotFound) + ); + + assert_eq!(events(), []); + }); +} + +#[test] +fn on_sector_terminate_invalid_caller() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + let _ = Market::add_balance(RuntimeOrigin::signed(account(PROVIDER)), 75); + System::reset_events(); + + let cid = BoundedVec::try_from(cid_of("polka_storage_cid").to_bytes()).unwrap(); + let sector_deal_ids: BoundedVec<_, ConstU32> = bounded_vec![1]; + + SectorDeals::::insert(cid.clone(), sector_deal_ids); + Proposals::::insert( + 1, + 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: 0, + end_block: 10, + storage_price_per_block: 10, + provider_collateral: 15, + state: DealState::Unpublished, + }, + ); + + assert_err!( + Market::on_sectors_terminate(&account(BOB), bounded_vec![cid],), + DispatchError::from(SectorTerminateError::InvalidCaller) + ); + + assert_eq!(events(), []); + }); +} + +#[test] +fn on_sector_terminate_not_active() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + let _ = Market::add_balance(RuntimeOrigin::signed(account(PROVIDER)), 75); + System::reset_events(); + + let cid = BoundedVec::try_from(cid_of("polka_storage_cid").to_bytes()).unwrap(); + let sector_deal_ids: BoundedVec<_, ConstU32> = bounded_vec![1]; + + SectorDeals::::insert(cid.clone(), sector_deal_ids); + Proposals::::insert( + 1, + 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: 0, + end_block: 10, + storage_price_per_block: 10, + provider_collateral: 15, + state: DealState::Unpublished, + }, + ); + + assert_err!( + Market::on_sectors_terminate( + &account(PROVIDER), + bounded_vec![cid], + ), + DispatchError::from(SectorTerminateError::DealIsNotActive) + ); + + assert_eq!(events(), []); + }); +} + +#[test] +fn on_sector_terminate_active() { + let _ = env_logger::try_init(); + new_test_ext().execute_with(|| { + let _ = Market::add_balance(RuntimeOrigin::signed(account(BOB)), 75); + let _ = Market::add_balance(RuntimeOrigin::signed(account(PROVIDER)), 75); + + let cid = BoundedVec::try_from(cid_of("polka_storage_cid").to_bytes()).unwrap(); + let sector_deal_ids: BoundedVec<_, ConstU32> = bounded_vec![1]; + let deal_proposal = DealProposal { + piece_cid: cid_of("polka_storage_piece") + .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: 0, + end_block: 10, + storage_price_per_block: 5, + provider_collateral: 15, + state: DealState::Active(ActiveDealState::new(0, 0)), + }; + + assert_ok!(lock_funds::(&account(BOB), 5 * 10)); + assert_ok!(lock_funds::(&account(PROVIDER), 15)); + + let hash_proposal = Market::hash_proposal(&deal_proposal); + let mut pending = PendingProposals::::get(); + pending + .try_insert(hash_proposal) + .expect("should have enough space"); + PendingProposals::::set(pending); + + SectorDeals::::insert(cid.clone(), sector_deal_ids); + Proposals::::insert(1, deal_proposal); + + System::reset_events(); + + assert_ok!(Market::on_sectors_terminate( + &account(PROVIDER), + bounded_vec![cid], + )); + + assert_eq!( + BalanceTable::::get(&account(BOB)), + BalanceEntry { + free: 70, // unlocked funds - 5 for the storage payment of a single block + locked: 0, // unlocked + } + ); + + assert_eq!( + BalanceTable::::get(&account(PROVIDER)), + BalanceEntry { + free: 65, // the original 60 + 5 for the storage payment of a single block + locked: 0, // lost the 15 collateral + } + ); + + assert_eq!( + events(), + [ + RuntimeEvent::Balances(pallet_balances::Event::::Withdraw { + who: Market::account_id(), + amount: 15 + }), + RuntimeEvent::Market(Event::::DealTerminated { + deal_id: 1, + client: account(BOB), + provider: account(PROVIDER) + }) + ] + ); + assert!(PendingProposals::::get().is_empty()); + assert!(!Proposals::::contains_key(1)); + assert_eq!( + ::Currency::total_issuance(), + 285 + ); + }); +} diff --git a/primitives/proofs/src/traits.rs b/primitives/proofs/src/traits.rs index 37106f800..5e0d82488 100644 --- a/primitives/proofs/src/traits.rs +++ b/primitives/proofs/src/traits.rs @@ -1,9 +1,15 @@ use cid::Cid; use sp_core::ConstU32; -use sp_runtime::{BoundedVec, DispatchError, RuntimeDebug}; +use sp_runtime::{BoundedVec, DispatchError, DispatchResult, RuntimeDebug}; use crate::types::{DealId, RegisteredSealProof, SectorNumber}; +/// Size of a CID with a 512-bit multihash — i.e. the default CID size. +const CID_SIZE_IN_BYTES: u32 = 64; + +/// The CID (in bytes) of a given sector. +pub type SectorId = BoundedVec>; + /// Number of Sectors that can be provided in a single extrinsics call. /// Required for BoundedVec. /// It was selected arbitrarly, without precise calculations. @@ -36,6 +42,17 @@ pub trait Market { sector_deals: BoundedVec, ConstU32>, compute_cid: bool, ) -> Result, ConstU32>, DispatchError>; + + /// Activate a set of deals grouped by sector, returning the size and + /// extra info about verified deals. + /// Sectors' deals are activated in parameter-defined order. + /// Each sector's deals are activated or fail as a group, but independently of other sectors. + /// Note that confirming all deals fit within a sector is the caller's responsibility + /// (and is implied by confirming the sector's data commitment is derived from the deal pieces). + fn on_sectors_terminate( + storage_provider: &AccountId, + sector_ids: BoundedVec>, + ) -> DispatchResult; } /// Binds given Sector with the Deals that it should contain