Skip to content

Commit

Permalink
Merge pull request #96 from eigerco/feat/11/market-pallet-activate-deals
Browse files Browse the repository at this point in the history
feat(pallet-market): implement activate_deals
  • Loading branch information
th7nder authored Jun 28, 2024
2 parents ee54b73 + 6d30641 commit 948bdc7
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 26 deletions.
172 changes: 155 additions & 17 deletions pallets/market/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ pub mod pallet {
use frame_system::{pallet_prelude::*, Config as SystemConfig, Pallet as System};
use multihash_codetable::{Code, MultihashDigest};
use primitives_proofs::{
DealId, Market, RegisteredSealProof, SectorDeal, SectorNumber, SectorSize,
ActiveDeal, ActiveSector, DealId, Market, RegisteredSealProof, SectorDeal, SectorNumber,
SectorSize, MAX_DEALS_FOR_ALL_SECTORS, MAX_DEALS_PER_SECTOR, MAX_SECTORS_PER_CALL,
};
use scale_info::TypeInfo;
use sp_arithmetic::traits::BaseArithmetic;
Expand Down Expand Up @@ -137,6 +138,20 @@ pub mod pallet {
slash_block: Option<BlockNumber>,
}

impl<BlockNumber> ActiveDealState<BlockNumber> {
fn new(
sector_number: SectorNumber,
sector_start_block: BlockNumber,
) -> ActiveDealState<BlockNumber> {
ActiveDealState {
sector_number,
sector_start_block,
last_updated_block: None,
slash_block: None,
}
}
}

#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
/// Reference: <https://github.com/filecoin-project/builtin-actors/blob/17ede2b256bc819dc309edf38e031e246a516486/actors/market/src/deal.rs#L93>
// It cannot be generic over <T: Config> because, #[derive(RuntimeDebug, TypeInfo)] also make `T` to have `RuntimeDebug`/`TypeInfo`
Expand Down Expand Up @@ -253,6 +268,12 @@ pub mod pallet {
client: T::AccountId,
provider: T::AccountId,
},
// Deal has been successfully activated.
DealActivated {
deal_id: DealId,
client: T::AccountId,
provider: T::AccountId,
},
}

#[pallet::error]
Expand Down Expand Up @@ -292,6 +313,10 @@ pub mod pallet {
SectorExpiresBeforeDeal,
/// Deal needs to be [`DealState::Published`] if it's to be activated
InvalidDealState,
/// Tried to activate a deal which is not in the system.
DealNotFound,
/// Tried to activate a deal which is not in the Pending Proposals
DealNotPending,
}

// NOTE(@th7nder,18/06/2024):
Expand Down Expand Up @@ -548,13 +573,28 @@ pub mod pallet {
DealActivationError::SectorExpiresBeforeDeal
);

// Confirm the deal is in the pending proposals set.
// It will be removed from this queue later, during cron.
// Failing this check is an internal invariant violation.
// The pending deals set exists to prevent duplicate proposals.
// It should be impossible to have a proposal, no deal state, and not be in pending deals.
let hash = Self::hash_proposal(&deal);
ensure!(
PendingProposals::<T>::get().contains(&hash),
DealActivationError::DealNotPending
);

Ok(())
}

fn proposals_for_deals(
deal_ids: BoundedVec<DealId, ConstU32<128>>,
) -> Result<BoundedVec<(DealId, DealProposalOf<T>), ConstU32<32>>, DispatchError> {
let mut unique_deals: BoundedBTreeSet<DealId, ConstU32<32>> = BoundedBTreeSet::new();
deal_ids: BoundedVec<DealId, ConstU32<MAX_DEALS_PER_SECTOR>>,
) -> Result<
BoundedVec<(DealId, DealProposalOf<T>), ConstU32<MAX_SECTORS_PER_CALL>>,
DispatchError,
> {
let mut unique_deals: BoundedBTreeSet<DealId, ConstU32<MAX_SECTORS_PER_CALL>> =
BoundedBTreeSet::new();
let mut proposals = BoundedVec::new();
for deal_id in deal_ids {
ensure!(!unique_deals.contains(&deal_id), {
Expand Down Expand Up @@ -669,7 +709,7 @@ pub mod pallet {
Error::<T>::ProposalsNotPublishedByStorageProvider
);

// TODO(@th7nder,#87,17/06/2024): validate a Storage Provider's Account (whether the account was registerd as Storage Provider)
// TODO(@th7nder,#87,17/06/2024): validate a Storage Provider's Account (whether the account was registered as Storage Provider)

let mut total_client_lockup: BoundedBTreeMap<T::AccountId, BalanceOf<T>, T::MaxDeals> =
BoundedBTreeMap::new();
Expand Down Expand Up @@ -710,17 +750,19 @@ pub mod pallet {
return None;
}

let hash = Self::hash_proposal(&deal);
let hash = Self::hash_proposal(&deal.proposal);
let duplicate_in_state = PendingProposals::<T>::get().contains(&hash);
let duplicate_in_message = message_proposals.contains(&hash);
if duplicate_in_state || duplicate_in_message {
log::error!(target: LOG_TARGET, "invalid deal: cannot publish duplicate deal idx: {}", idx);
return None;
}
if let Err(e) = PendingProposals::<T>::get().try_insert(hash) {
let mut pending = PendingProposals::<T>::get();
if let Err(e) = pending.try_insert(hash) {
log::error!(target: LOG_TARGET, "cannot publish: too many pending deal proposals, wait for them to be expired/activated, deal idx: {}, err: {:?}", idx, e);
return None;
}
PendingProposals::<T>::set(pending);
// PRE-COND: always succeeds, as there cannot be more deals than T::MaxDeals and this the size of the set
message_proposals.try_insert(hash).ok()?;
// PRE-COND: always succeeds as there cannot be more clients than T::MaxDeals
Expand All @@ -735,16 +777,11 @@ pub mod pallet {
}

// Used for deduplication purposes
// We don't want to store another BTreeSet of ClientDealProposals
// We don't want to store another BTreeSet of DealProposals
// We only care about hashes.
// It is not an associated function, because T::Hashing is hard to use inside of there.
fn hash_proposal(
proposal: &ClientDealProposal<
T::AccountId,
BalanceOf<T>,
BlockNumberFor<T>,
T::OffchainSignature,
>,
pub(crate) fn hash_proposal(
proposal: &DealProposal<T::AccountId, BalanceOf<T>, BlockNumberFor<T>>,
) -> T::Hash {
let bytes = Encode::encode(proposal);
T::Hashing::hash(&bytes)
Expand All @@ -757,8 +794,9 @@ pub mod pallet {
/// Currently UnsealedCID is hardcoded as we `compute_commd` remains unimplemented because of #92.
fn verify_deals_for_activation(
storage_provider: &T::AccountId,
sector_deals: BoundedVec<SectorDeal<BlockNumberFor<T>>, ConstU32<32>>,
) -> Result<BoundedVec<Option<Cid>, ConstU32<32>>, DispatchError> {
sector_deals: BoundedVec<SectorDeal<BlockNumberFor<T>>, ConstU32<MAX_SECTORS_PER_CALL>>,
) -> Result<BoundedVec<Option<Cid>, ConstU32<MAX_SECTORS_PER_CALL>>, DispatchError>
{
let curr_block = System::<T>::block_number();
let mut unsealed_cids = BoundedVec::new();
for sector in sector_deals {
Expand Down Expand Up @@ -791,5 +829,105 @@ 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<SectorDeal<BlockNumberFor<T>>, ConstU32<MAX_SECTORS_PER_CALL>>,
compute_cid: bool,
) -> Result<
BoundedVec<ActiveSector<T::AccountId>, ConstU32<MAX_SECTORS_PER_CALL>>,
DispatchError,
> {
// TODO(@th7nder,#87,17/06/2024): validate a Storage Provider's Account (whether the account was registered as Storage Provider)
let mut activations = BoundedVec::new();
let curr_block = System::<T>::block_number();
let mut activated_deal_ids: BoundedBTreeSet<
DealId,
ConstU32<MAX_DEALS_FOR_ALL_SECTORS>,
> = BoundedBTreeSet::new();

for sector in sector_deals {
let proposals = Self::proposals_for_deals(sector.deal_ids)?;
let sector_size = sector.sector_type.sector_size();
if let Err(e) = Self::validate_deals_for_sector(
&proposals,
storage_provider,
sector.sector_number,
sector.sector_expiry,
curr_block,
sector_size,
) {
log::error!(
"failed to activate sector: {}, skipping... {:?}",
sector.sector_number,
e
);
continue;
}

let data_commitment = if compute_cid && !proposals.is_empty() {
Some(Self::compute_commd(
proposals.iter().map(|(_, deal)| deal),
sector.sector_type,
)?)
} else {
None
};

let mut activated_deals: BoundedVec<_, ConstU32<MAX_DEALS_PER_SECTOR>> =
BoundedVec::new();
for (deal_id, mut proposal) in proposals {
// Make it Active! This is what's this function is about in the end.
proposal.state =
DealState::Active(ActiveDealState::new(sector.sector_number, curr_block));

activated_deals
.try_push(ActiveDeal {
client: proposal.client.clone(),
piece_cid: proposal.cid().map_err(|e| {
log::error!(
"there is invalid cid saved on-chain for deal: {}, {:?}",
deal_id,
e
);
Error::<T>::DealPreconditionFailed
})?,
piece_size: proposal.piece_size,
})
.map_err(|_| {
log::error!("failed to insert into `activated`, programmer's error");
Error::<T>::DealPreconditionFailed
})?;
activated_deal_ids.try_insert(deal_id).map_err(|_| {
log::error!(
"failed to insert into `activated_deal_ids`, programmer's error"
);
Error::<T>::DealPreconditionFailed
})?;

Self::deposit_event(Event::<T>::DealActivated {
deal_id,
client: proposal.client.clone(),
provider: proposal.provider.clone(),
});
Proposals::<T>::insert(deal_id, proposal);
}

activations
.try_push(ActiveSector {
active_deals: activated_deals,
unsealed_cid: data_commitment,
})
.map_err(|_| Error::<T>::DealPreconditionFailed)?;
}

Ok(activations)
}
}
}
2 changes: 1 addition & 1 deletion pallets/market/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub fn cid_of(data: &str) -> cid::Cid {
Cid::new_v1(CID_CODEC, Code::Blake2b256.digest(data.as_bytes()))
}

type DealProposalOf<T> =
pub(crate) type DealProposalOf<T> =
DealProposal<<T as frame_system::Config>::AccountId, BalanceOf<T>, BlockNumberFor<T>>;

type ClientDealProposalOf<T> = ClientDealProposal<
Expand Down
91 changes: 88 additions & 3 deletions pallets/market/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ use cid::Cid;
use frame_support::{
assert_noop, assert_ok,
sp_runtime::{bounded_vec, ArithmeticError, TokenError},
BoundedBTreeSet,
};
use primitives_proofs::{
ActiveDeal, ActiveSector, DealId, Market as MarketTrait, RegisteredSealProof, SectorDeal,
};
use primitives_proofs::{Market as MarketTrait, RegisteredSealProof, SectorDeal};

use crate::{
mock::*, BalanceEntry, BalanceTable, DealProposal, DealState, Error, Event, Proposals,
mock::*, BalanceEntry, BalanceTable, DealProposal, DealState, Error, Event, PendingProposals,
Proposals,
};

#[test]
Expand Down Expand Up @@ -230,6 +234,8 @@ fn publish_storage_deals() {
state: DealState::Unpublished,
},
);
let alice_hash = Market::hash_proposal(&alice_proposal.proposal);
let bob_hash = Market::hash_proposal(&bob_proposal.proposal);

let _ = Market::add_balance(RuntimeOrigin::signed(account(ALICE)), 60);
let _ = Market::add_balance(RuntimeOrigin::signed(account(BOB)), 70);
Expand Down Expand Up @@ -277,14 +283,16 @@ fn publish_storage_deals() {
}),
]
);
assert!(PendingProposals::<Test>::get().contains(&alice_hash));
assert!(PendingProposals::<Test>::get().contains(&bob_hash));
});
}

#[test]
fn verify_deals_for_activation() {
let _ = env_logger::try_init();
new_test_ext().execute_with(|| {
Proposals::<Test>::insert(
publish_for_activation(
1,
DealProposal {
piece_cid: cid_of("polka-storage-data")
Expand Down Expand Up @@ -330,3 +338,80 @@ fn verify_deals_for_activation() {
);
});
}

#[test]
fn activate_deals() {
let _ = env_logger::try_init();
new_test_ext().execute_with(|| {
publish_for_activation(
1,
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::Published,
},
);

let deals = bounded_vec![
SectorDeal {
sector_number: 1,
sector_expiry: 120,
sector_type: RegisteredSealProof::StackedDRG2KiBV1P1,
deal_ids: bounded_vec![1]
},
SectorDeal {
sector_number: 2,
sector_expiry: 50,
sector_type: RegisteredSealProof::StackedDRG2KiBV1P1,
deal_ids: bounded_vec![]
}
];

let piece_cid =
Cid::from_str("bafk2bzacecg3xxc4f2ql2hreiuy767u6r72ekdz54k7luieknboaakhft5rgk")
.unwrap();
let placeholder_commd_cid =
Cid::from_str("bafk2bzaceajreoxfdcpdvitpvxm7vkpvcimlob5ejebqgqidjkz4qoug4q6zu")
.unwrap();
assert_eq!(
Ok(bounded_vec![
ActiveSector {
active_deals: bounded_vec![ActiveDeal {
client: account(ALICE),
piece_cid: piece_cid,
piece_size: 18
}],
unsealed_cid: Some(placeholder_commd_cid),
},
ActiveSector {
active_deals: bounded_vec![],
unsealed_cid: None
}
]),
Market::activate_deals(&account(PROVIDER), deals, true)
);
});
}

/// Creates a new deal and saves it in the Runtime Storage.
/// In addition to saving it to `Proposals::<T>` it also calculate's
/// it's hash and saves it to `PendingProposals::<T>`.
/// Behaves like `publish_storage_deals` without the validation and calling extrinsics.
fn publish_for_activation(deal_id: DealId, deal: DealProposalOf<Test>) {
let hash = Market::hash_proposal(&deal);
let mut pending = BoundedBTreeSet::new();
pending.try_insert(hash).unwrap();
PendingProposals::<Test>::set(pending);

Proposals::<Test>::insert(deal_id, deal);
}
Loading

0 comments on commit 948bdc7

Please sign in to comment.