Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pallet-market): implement activate_deals #96

Merged
merged 9 commits into from
Jun 28, 2024
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() {
jmg-duarte marked this conversation as resolved.
Show resolved Hide resolved
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));
jmg-duarte marked this conversation as resolved.
Show resolved Hide resolved

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