diff --git a/actors/market/src/lib.rs b/actors/market/src/lib.rs index 376c231fb..6f08d20b5 100644 --- a/actors/market/src/lib.rs +++ b/actors/market/src/lib.rs @@ -80,9 +80,7 @@ impl Actor { pub fn constructor(rt: &mut impl Runtime) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; - let st = State::new(rt.store()).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "Failed to create market state") - })?; + let st = State::new(rt.store())?; rt.create(&st)?; Ok(()) } @@ -105,23 +103,7 @@ impl Actor { let (nominal, _, _) = escrow_address(rt, &provider_or_client)?; rt.transaction(|st: &mut State, rt| { - let mut msm = st.mutator(rt.store()); - msm.with_escrow_table(Permission::Write) - .with_locked_table(Permission::Write) - .build() - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load state") - })?; - - msm.escrow_table.as_mut().unwrap().add(&nominal, &msg_value).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to add balance to escrow table", - ) - })?; - - msm.commit_state()?; - + st.add_balance_to_escrow_table(rt.store(), &nominal, &msg_value)?; Ok(()) })?; @@ -144,34 +126,7 @@ impl Actor { rt.validate_immediate_caller_is(&approved)?; let amount_extracted = rt.transaction(|st: &mut State, rt| { - let mut msm = st.mutator(rt.store()); - msm.with_escrow_table(Permission::Write) - .with_locked_table(Permission::Write) - .build() - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load state") - })?; - - // The withdrawable amount might be slightly less than nominal - // depending on whether or not all relevant entries have been processed - // by cron - let min_balance = msm.locked_table.as_ref().unwrap().get(&nominal).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get locked balance") - })?; - - let ex = msm - .escrow_table - .as_mut() - .unwrap() - .subtract_with_minimum(&nominal, ¶ms.amount, &min_balance) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to subtract from escrow table", - ) - })?; - - msm.commit_state()?; + let ex = st.withdraw_balance_from_escrow_table(rt.store(), &nominal, ¶ms.amount)?; Ok(ex) })?; @@ -247,14 +202,7 @@ impl Actor { let mut valid_input_bf = BitField::default(); let curr_epoch = rt.curr_epoch(); - let mut state: State = rt.state::()?; - let store = rt.store(); - let mut msm = state.mutator(store); - msm.with_pending_proposals(Permission::ReadOnly) - .with_escrow_table(Permission::ReadOnly) - .with_locked_table(Permission::ReadOnly) - .build() - .map_err(|e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load msm"))?; + let state: State = rt.state()?; for (di, mut deal) in params.deals.into_iter().enumerate() { // drop malformed deals @@ -289,12 +237,7 @@ impl Actor { client_lockup += deal.proposal.client_balance_requirement(); let client_balance_ok = - msm.balance_covered(Address::new_id(client_id), &client_lockup).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to check client balance coverage", - ) - })?; + state.balance_covered(rt.store(), Address::new_id(client_id), &client_lockup)?; if !client_balance_ok { info!("invalid deal: {}: insufficient client funds to cover proposal cost", di); @@ -303,14 +246,11 @@ impl Actor { let mut provider_lockup = total_provider_lockup.clone(); provider_lockup += &deal.proposal.provider_collateral; - let provider_balance_ok = msm - .balance_covered(Address::new_id(provider_id), &provider_lockup) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to check provider balance coverage", - ) - })?; + let provider_balance_ok = state.balance_covered( + rt.store(), + Address::new_id(provider_id), + &provider_lockup, + )?; if !provider_balance_ok { info!("invalid deal: {}: insufficient provider funds to cover proposal cost", di); @@ -328,13 +268,8 @@ impl Actor { // check proposalCids for duplication within message batch // check state PendingProposals for duplication across messages - let duplicate_in_state = - msm.pending_deals.as_ref().unwrap().has(&pcid.to_bytes()).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to check for existence of deal proposal", - ) - })?; + let duplicate_in_state = state.has_pending_deal(rt.store(), pcid)?; + let duplicate_in_message = proposal_cid_lookup.contains(&pcid); if duplicate_in_state || duplicate_in_message { info!("invalid deal {}: cannot publish duplicate deal proposal", di); @@ -413,63 +348,47 @@ impl Actor { let mut new_deal_ids = Vec::with_capacity(valid_deals.len()); rt.transaction(|st: &mut State, rt| { - let mut msm = st.mutator(rt.store()); - msm.with_pending_proposals(Permission::Write) - .with_deal_proposals(Permission::Write) - .with_deals_by_epoch(Permission::Write) - .with_escrow_table(Permission::Write) - .with_locked_table(Permission::Write) - .build() - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load state") - })?; + let mut pending_deals: Vec = vec![]; + let mut deal_proposals: Vec<(DealID, DealProposal)> = vec![]; + let mut deals_by_epoch: Vec<(ChainEpoch, DealID)> = vec![]; + let mut pending_deal_allocation_ids: Vec<(BytesKey, AllocationID)> = vec![]; + // All storage dealProposals will be added in an atomic transaction; this operation will be unrolled if any of them fails. // This should only fail on programmer error because all expected invalid conditions should be filtered in the first set of checks. for valid_deal in valid_deals.iter() { - msm.lock_client_and_provider_balances(&valid_deal.proposal)?; + st.lock_client_and_provider_balances(rt.store(), &valid_deal.proposal)?; // Store the proposal CID in pending deals set. - msm.pending_deals - .as_mut() - .unwrap() - .put(valid_deal.cid.to_bytes().into()) - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set pending deal")?; + pending_deals.push(valid_deal.cid); + // Allocate a deal ID and store the proposal in the proposals AMT. - let deal_id = msm.generate_storage_deal_id(); - msm.deal_proposals - .as_mut() - .unwrap() - .set(deal_id, valid_deal.proposal.clone()) - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal")?; + let deal_id = st.generate_storage_deal_id(); + deal_proposals.push((deal_id, valid_deal.proposal.clone())); + // Store verified allocation (if any) in the pending allocation IDs map. // It will be removed when the deal is activated or expires. if valid_deal.allocation != NO_ALLOCATION_ID { - msm.pending_deal_allocation_ids - .as_mut() - .unwrap() - .set(deal_id_key(deal_id), valid_deal.allocation) - .context_code( - ExitCode::USR_ILLEGAL_STATE, - "failed to set deal allocation", - )?; + pending_deal_allocation_ids.push((deal_id_key(deal_id), valid_deal.allocation)); } // Randomize the first epoch for when the deal will be processed so an attacker isn't able to // schedule too many deals for the same tick. - let process_epoch = - gen_rand_next_epoch(rt.policy(), valid_deal.proposal.start_epoch, deal_id); - - msm.deals_by_epoch.as_mut().unwrap().put(process_epoch, deal_id).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to set deal ops by epoch", - ) - })?; + deals_by_epoch.push(( + gen_rand_next_epoch(rt.policy(), valid_deal.proposal.start_epoch, deal_id), + deal_id, + )); new_deal_ids.push(deal_id); } - msm.commit_state()?; + st.put_pending_deals(rt.store(), &pending_deals)?; + + st.put_deal_proposals(rt.store(), &deal_proposals)?; + + st.put_pending_deal_allocation_ids(rt.store(), &pending_deal_allocation_ids)?; + + st.put_deals_by_epoch(rt.store(), &deals_by_epoch)?; + Ok(()) })?; @@ -487,9 +406,7 @@ impl Actor { let curr_epoch = rt.curr_epoch(); let st: State = rt.state()?; - let proposals = DealArray::load(&st.proposals, rt.store()).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposals") - })?; + let proposals = st.get_proposal_array(rt.store())?; let mut sectors_data = Vec::with_capacity(params.sectors.len()); for sector in params.sectors.iter() { @@ -527,12 +444,10 @@ impl Actor { let miner_addr = rt.message().caller(); let curr_epoch = rt.curr_epoch(); - let deal_spaces = { - let st: State = rt.state()?; - let proposals = DealArray::load(&st.proposals, rt.store()).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposals") - })?; + let st: State = rt.state()?; + let proposals = st.get_proposal_array(rt.store())?; + let deal_spaces = { validate_and_return_deal_space( &proposals, ¶ms.deal_ids, @@ -547,26 +462,13 @@ impl Actor { // Update deal states let mut verified_infos = Vec::new(); rt.transaction(|st: &mut State, rt| { - let mut msm = st.mutator(rt.store()); - msm.with_deal_states(Permission::Write) - .with_pending_proposals(Permission::Write) - .with_deal_proposals(Permission::ReadOnly) - .build() - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load state") - })?; + let mut deal_states: Vec<(DealID, DealState)> = vec![]; for deal_id in params.deal_ids { // This construction could be replaced with a single "update deal state" // state method, possibly batched over all deal ids at once. - let s = msm - .deal_states - .as_ref() - .unwrap() - .get(deal_id) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to check state for deal ({})", deal_id) - })?; + let s = st.find_deal_state(rt.store(), deal_id)?; + if s.is_some() { return Err(actor_error!( illegal_argument, @@ -575,28 +477,15 @@ impl Actor { )); } - let proposal = msm - .deal_proposals - .as_ref() - .unwrap() - .get(deal_id) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to load deal proposal {}", deal_id) - })? - .ok_or_else(|| actor_error!(not_found, "no such deal proposal {}", deal_id))?; + let proposal = st + .find_proposal(rt.store(), deal_id)? + .ok_or_else(|| actor_error!(not_found, "no such deal_id: {}", deal_id))?; - let propc = rt_deal_cid(rt, proposal)?; + let propc = rt_deal_cid(rt, &proposal)?; // Confirm the deal is in the pending proposals queue. // It will be removed from this queue later, during cron. - let has = msm - .pending_deals - .as_ref() - .unwrap() - .has(&propc.to_bytes()) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to get pending proposal ({})", propc) - })?; + let has = st.has_pending_deal(rt.store(), propc)?; if !has { return Err(actor_error!( @@ -607,16 +496,11 @@ impl Actor { } // Extract and remove any verified allocation ID for the pending deal. - let allocation = msm - .pending_deal_allocation_ids - .as_mut() - .unwrap() - .delete(&deal_id_key(deal_id)) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to remove allocation id for deal {}", deal_id) - })? + let allocation = st + .remove_pending_deal_allocation_id(rt.store(), &deal_id_key(deal_id))? .unwrap_or((BytesKey(vec![]), NO_ALLOCATION_ID)) .1; + if allocation != NO_ALLOCATION_ID { verified_infos.push(VerifiedDealInfo { client: proposal.client.id().unwrap(), @@ -625,24 +509,20 @@ impl Actor { size: proposal.piece_size, }) } - msm.deal_states - .as_mut() - .unwrap() - .set( - deal_id, - DealState { - sector_start_epoch: curr_epoch, - last_updated_epoch: EPOCH_UNDEFINED, - slash_epoch: EPOCH_UNDEFINED, - verified_claim: allocation, - }, - ) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to set deal state {}", deal_id) - })?; + + deal_states.push(( + deal_id, + DealState { + sector_start_epoch: curr_epoch, + last_updated_epoch: EPOCH_UNDEFINED, + slash_epoch: EPOCH_UNDEFINED, + verified_claim: allocation, + }, + )); } - msm.commit_state()?; + st.put_deal_states(rt.store(), &deal_states)?; + Ok(()) })?; @@ -660,18 +540,11 @@ impl Actor { let miner_addr = rt.message().caller(); rt.transaction(|st: &mut State, rt| { - let mut msm = st.mutator(rt.store()); - msm.with_deal_states(Permission::Write) - .with_deal_proposals(Permission::ReadOnly) - .build() - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load state") - })?; + let mut deal_states: Vec<(DealID, DealState)> = vec![]; for id in params.deal_ids { - let deal = msm.deal_proposals.as_ref().unwrap().get(id).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get deal proposal") - })?; + let deal = st.find_proposal(rt.store(), id)?; + // The deal may have expired and been deleted before the sector is terminated. // Nothing to do, but continue execution for the other deals. if deal.is_none() { @@ -696,14 +569,8 @@ impl Actor { continue; } - let mut state: DealState = *msm - .deal_states - .as_ref() - .unwrap() - .get(id) - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get deal state") - })? + let mut state: DealState = st + .find_deal_state(rt.store(), id)? // A deal with a proposal but no state is not activated, but then it should not be // part of a sector that is terminating. .ok_or_else(|| actor_error!(illegal_argument, "no state for deal {}", id))?; @@ -718,15 +585,10 @@ impl Actor { // and slashing of provider collateral happens in cron_tick. state.slash_epoch = params.epoch; - msm.deal_states.as_mut().unwrap().set(id, state).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to set deal state ({})", id), - ) - })?; + deal_states.push((id, state)); } - msm.commit_state()?; + st.put_deal_states(rt.store(), &deal_states)?; Ok(()) })?; Ok(()) @@ -739,10 +601,8 @@ impl Actor { rt.validate_immediate_caller_type(std::iter::once(&Type::Miner))?; let st: State = rt.state()?; + let proposals = st.get_proposal_array(rt.store())?; - let proposals = DealArray::load(&st.proposals, rt.store()).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposals") - })?; let mut commds = Vec::with_capacity(params.inputs.len()); for comm_input in params.inputs.iter() { commds.push(compute_data_commitment( @@ -765,57 +625,19 @@ impl Actor { rt.transaction(|st: &mut State, rt| { let last_cron = st.last_cron; let mut updates_needed: BTreeMap> = BTreeMap::new(); - let mut msm = st.mutator(rt.store()); - msm.with_deal_states(Permission::Write) - .with_locked_table(Permission::Write) - .with_escrow_table(Permission::Write) - .with_deals_by_epoch(Permission::Write) - .with_deal_proposals(Permission::Write) - .with_pending_proposals(Permission::Write) - .build() - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to load state") - })?; + let mut rm_cron_id: Vec = vec![]; for i in (last_cron + 1)..=rt.curr_epoch() { - // TODO specs-actors modifies msm as it's iterated through, which is memory unsafe - // for now the deal ids are being collected and then iterated on, which could - // cause a potential inconsistency in exit code returned if a deal_id fails - // to be pulled from storage where it wouldn't be triggered otherwise. - // Workaround a better solution (seperating msm or fixing go impl) - let mut deal_ids = Vec::new(); - msm.deals_by_epoch - .as_ref() - .unwrap() - .for_each(i, |deal_id| { - deal_ids.push(deal_id); - Ok(()) - }) - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal state")?; + let deal_ids = st.get_deals_for_epoch(rt.store(), i)?; for deal_id in deal_ids { - let deal = msm - .deal_proposals - .as_ref() - .unwrap() - .get(deal_id) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to get deal_id ({})", deal_id) - })? - .ok_or_else(|| { - actor_error!(not_found, "proposal doesn't exist ({})", deal_id) - })? - .clone(); + let deal = st.find_proposal(rt.store(), deal_id)?.ok_or_else(|| { + actor_error!(not_found, "proposal doesn't exist ({})", deal_id) + })?; let dcid = rt_deal_cid(rt, &deal)?; - let state = msm - .deal_states - .as_ref() - .unwrap() - .get(deal_id) - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get deal state")? - .cloned(); + let state = st.find_deal_state(rt.store(), deal_id)?; // deal has been published but not activated yet -> terminate it // as it has timed out @@ -830,20 +652,14 @@ impl Actor { )); } - let slashed = msm.process_deal_init_timed_out(&deal)?; + let slashed = st.process_deal_init_timed_out(rt.store(), &deal)?; if !slashed.is_zero() { amount_slashed += slashed; } // Delete the proposal (but not state, which doesn't exist). - let deleted = msm - .deal_proposals - .as_mut() - .unwrap() - .delete(deal_id) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to delete deal proposal {}", deal_id) - })?; + let deleted = st.remove_proposal(rt.store(), deal_id)?; + if deleted.is_none() { return Err(actor_error!( illegal_state, @@ -853,57 +669,39 @@ impl Actor { ) )); } + // Delete pending deal CID - msm.pending_deals - .as_mut() - .unwrap() - .delete(&dcid.to_bytes()) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to delete pending proposal {}", deal_id) - })? - .ok_or_else(|| { - actor_error!( - illegal_state, - "failed to delete pending proposal: does not exist" - ) - })?; + st.remove_pending_deal(rt.store(), dcid)?.ok_or_else(|| { + actor_error!( + illegal_state, + "failed to delete pending deals: does not exist" + ) + })?; + // Delete pending deal allocation id (if present). - msm.pending_deal_allocation_ids - .as_mut() - .unwrap() - .delete(&deal_id_key(deal_id)) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!( - "failed to delete pending proposal allocation id for {}", - deal_id - ) - })?; + st.remove_pending_deal_allocation_id(rt.store(), &deal_id_key(deal_id))?; continue; } let mut state = state.unwrap(); if state.last_updated_epoch == EPOCH_UNDEFINED { - msm.pending_deals - .as_mut() - .unwrap() - .delete(&dcid.to_bytes()) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to delete pending proposal {}", dcid), - ) - })? - .ok_or_else(|| { - actor_error!( - illegal_state, - "failed to delete pending proposal: does not exist" - ) - })?; + st.remove_pending_deal(rt.store(), dcid)?.ok_or_else(|| { + actor_error!( + illegal_state, + "failed to delete pending proposal: does not exist" + ) + })?; } - let (slash_amount, next_epoch, remove_deal) = - msm.update_pending_deal_state(rt.policy(), &state, &deal, curr_epoch)?; + let (slash_amount, next_epoch, remove_deal) = st.put_pending_deal_state( + rt.store(), + rt.policy(), + &state, + &deal, + curr_epoch, + )?; + if slash_amount.is_negative() { return Err(actor_error!( illegal_state, @@ -928,13 +726,8 @@ impl Actor { amount_slashed += slash_amount; // Delete proposal and state simultaneously. - let deleted = - msm.deal_states.as_mut().unwrap().delete(deal_id).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to delete deal state", - ) - })?; + let deleted = st.remove_deal_state(rt.store(), deal_id)?; + if deleted.is_none() { return Err(actor_error!( illegal_state, @@ -942,13 +735,8 @@ impl Actor { )); } - let deleted = - msm.deal_proposals.as_mut().unwrap().delete(deal_id).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to delete deal proposal", - ) - })?; + let deleted = st.remove_proposal(rt.store(), deal_id)?; + if deleted.is_none() { return Err(actor_error!( illegal_state, @@ -973,12 +761,7 @@ impl Actor { } state.last_updated_epoch = curr_epoch; - msm.deal_states.as_mut().unwrap().set(deal_id, state).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to set deal state", - ) - })?; + st.put_deal_states(rt.store(), &[(deal_id, state)])?; if let Some(ev) = updates_needed.get_mut(&next_epoch) { ev.push(deal_id); @@ -987,27 +770,16 @@ impl Actor { } } } - msm.deals_by_epoch.as_mut().unwrap().remove_all(i).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to delete deal ops for epoch {}", i), - ) - })?; + rm_cron_id.push(i); } + st.remove_deals_by_epoch(rt.store(), &rm_cron_id)?; + // updates_needed is already sorted by epoch. - for (epoch, deals) in updates_needed { - msm.deals_by_epoch.as_mut().unwrap().put_many(epoch, &deals).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to reinsert deal IDs for epoch {}", epoch), - ) - })?; - } + st.put_batch_deals_by_epoch(rt.store(), &updates_needed)?; - msm.st.last_cron = rt.curr_epoch(); + st.last_cron = rt.curr_epoch(); - msm.commit_state()?; Ok(()) })?; @@ -1025,6 +797,7 @@ fn compute_data_commitment( deal_ids: &[DealID], ) -> Result { let mut pieces = Vec::with_capacity(deal_ids.len()); + for deal_id in deal_ids { let deal = proposals .get(*deal_id) @@ -1035,6 +808,7 @@ fn compute_data_commitment( ) })? .ok_or_else(|| actor_error!(not_found, "proposal doesn't exist ({})", deal_id))?; + pieces.push(PieceInfo { cid: deal.piece_cid, size: deal.piece_size }); } rt.compute_unsealed_sector_cid(sector_type, &pieces).map_err(|e| { @@ -1053,6 +827,7 @@ pub fn validate_and_return_deal_space( let mut seen_deal_ids = BTreeSet::new(); let mut deal_space = BigInt::zero(); let mut verified_deal_space = BigInt::zero(); + for deal_id in deal_ids { if !seen_deal_ids.insert(deal_id) { return Err(actor_error!( @@ -1061,6 +836,7 @@ pub fn validate_and_return_deal_space( deal_id )); } + let proposal = proposals .get(*deal_id) .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal")? diff --git a/actors/market/src/state.rs b/actors/market/src/state.rs index b07703efa..902f7f63d 100644 --- a/actors/market/src/state.rs +++ b/actors/market/src/state.rs @@ -3,16 +3,16 @@ use crate::balance_table::BalanceTable; use crate::ext::verifreg::AllocationID; -use anyhow::anyhow; use cid::Cid; use fil_actors_runtime::runtime::Policy; use fil_actors_runtime::{ - actor_error, make_empty_map, make_map_with_root_and_bitwidth, ActorDowncast, ActorError, Array, - AsActorError, Map, Set, SetMultimap, + actor_error, make_empty_map, make_map_with_root_and_bitwidth, ActorError, Array, AsActorError, + Set, SetMultimap, }; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::tuple::*; use fvm_ipld_encoding::Cbor; +use fvm_ipld_hamt::BytesKey; use fvm_shared::address::Address; use fvm_shared::clock::{ChainEpoch, EPOCH_UNDEFINED}; use fvm_shared::deal::DealID; @@ -20,11 +20,18 @@ use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; use fvm_shared::HAMT_BIT_WIDTH; use num_traits::Zero; +use std::collections::BTreeMap; use super::policy::*; use super::types::*; use super::{DealProposal, DealState}; +pub enum Reason { + ClientCollateral, + ClientStorageFee, + ProviderCollateral, +} + /// Market actor state #[derive(Clone, Default, Serialize_tuple, Deserialize_tuple, Debug)] pub struct State { @@ -69,29 +76,43 @@ pub struct State { pub pending_deal_allocation_ids: Cid, // HAMT[DealID]AllocationID } +impl Cbor for State {} + impl State { - pub fn new(store: &BS) -> anyhow::Result { + pub fn new(store: &BS) -> Result { let empty_proposals_array = Array::<(), BS>::new_with_bit_width(store, PROPOSALS_AMT_BITWIDTH) .flush() - .map_err(|e| anyhow!("Failed to create empty proposals array: {}", e))?; + .context_code( + ExitCode::USR_ILLEGAL_STATE, + "Failed to create empty proposals array", + )?; + let empty_states_array = Array::<(), BS>::new_with_bit_width(store, STATES_AMT_BITWIDTH) .flush() - .map_err(|e| anyhow!("Failed to create empty states array: {}", e))?; + .context_code(ExitCode::USR_ILLEGAL_STATE, "Failed to create empty states array")?; + + let empty_pending_proposals_map = + make_empty_map::<_, ()>(store, HAMT_BIT_WIDTH).flush().context_code( + ExitCode::USR_ILLEGAL_STATE, + "Failed to create empty pending proposals map state", + )?; + + let empty_balance_table = BalanceTable::new(store).root().context_code( + ExitCode::USR_ILLEGAL_STATE, + "Failed to create empty balance table map", + )?; - let empty_pending_proposals_map = make_empty_map::<_, ()>(store, HAMT_BIT_WIDTH) - .flush() - .map_err(|e| anyhow!("Failed to create empty pending proposals map state: {}", e))?; - let empty_balance_table = BalanceTable::new(store) - .root() - .map_err(|e| anyhow!("Failed to create empty balance table map: {}", e))?; let empty_deal_ops_hamt = SetMultimap::new(store) .root() - .map_err(|e| anyhow!("Failed to create empty multiset: {}", e))?; + .context_code(ExitCode::USR_ILLEGAL_STATE, "Failed to create empty multiset")?; + let empty_pending_deal_allocation_map = - make_empty_map::<_, AllocationID>(store, HAMT_BIT_WIDTH).flush().map_err(|e| { - anyhow!("Failed to create empty pending deal allocation map: {}", e) - })?; + make_empty_map::<_, AllocationID>(store, HAMT_BIT_WIDTH).flush().context_code( + ExitCode::USR_ILLEGAL_STATE, + "Failed to create empty pending deal allocation map", + )?; + Ok(Self { proposals: empty_proposals_array, states: empty_states_array, @@ -109,271 +130,459 @@ impl State { }) } - pub fn total_locked(&self) -> TokenAmount { + pub fn get_total_locked(&self) -> TokenAmount { &self.total_client_locked_collateral + &self.total_provider_locked_collateral + &self.total_client_storage_fee } - pub(super) fn mutator<'bs, BS: Blockstore>( + pub fn find_deal_state( + &self, + store: &BS, + deal_id: DealID, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let states = DealMetaArray::load(&self.states, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal state array")?; + + let found = states.get(deal_id).with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("no such deal state for {}", deal_id) + })?; + + Ok(found.cloned()) + } + + pub fn put_deal_states( &mut self, - store: &'bs BS, - ) -> MarketStateMutation<'bs, '_, BS> { - MarketStateMutation::new(self, store) + store: &BS, + new_deal_states: &[(DealID, DealState)], + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut states = DealMetaArray::load(&self.states, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposal array")?; + + new_deal_states.iter().try_for_each(|(id, deal_state)| -> Result<(), ActorError> { + states + .set(*id, *deal_state) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal state")?; + Ok(()) + })?; + + self.states = states + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal states")?; + + Ok(()) } -} -fn deal_get_payment_remaining( - deal: &DealProposal, - mut slash_epoch: ChainEpoch, -) -> Result { - if slash_epoch > deal.end_epoch { - return Err(actor_error!( - illegal_state, - "deal slash epoch {} after end epoch {}", - slash_epoch, - deal.end_epoch - )); + pub fn remove_deal_state( + &mut self, + store: &BS, + deal_id: DealID, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let mut states = DealMetaArray::load(&self.states, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposal array")?; + + let rval_deal_state = states + .delete(deal_id) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to delete deal state")?; + + self.states = states + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal states")?; + + Ok(rval_deal_state) } - // Payments are always for start -> end epoch irrespective of when the deal is slashed. - slash_epoch = std::cmp::max(slash_epoch, deal.start_epoch); + pub fn get_proposal_array<'a, BS>(&'a self, store: &'a BS) -> Result, ActorError> + where + BS: Blockstore, + { + let deal_proposals = DealArray::load(&self.proposals, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposal array")?; - let duration_remaining = deal.end_epoch - slash_epoch; - if duration_remaining < 0 { - return Err(actor_error!( - illegal_state, - "deal remaining duration negative: {}", - duration_remaining - )); + Ok(deal_proposals) } - Ok(&deal.storage_price_per_epoch * duration_remaining as u64) -} + pub fn get_proposal( + &self, + store: &BS, + id: DealID, + ) -> Result { + let found = self + .find_proposal(store, id)? + .with_context_code(ExitCode::USR_NOT_FOUND, || format!("no such deal {}", id))?; + Ok(found) + } -impl Cbor for State {} + pub fn find_proposal( + &self, + store: &BS, + deal_id: DealID, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let deal_proposals = self.get_proposal_array(store)?; + + let proposal = + deal_proposals.get(deal_id).with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to load deal proposal {}", deal_id) + })?; -#[derive(Debug, PartialEq, Eq)] -pub(super) enum Permission { - Invalid, - ReadOnly, - Write, -} + Ok(proposal.cloned()) + } -pub(super) enum Reason { - ClientCollateral, - ClientStorageFee, - ProviderCollateral, -} + pub fn remove_proposal( + &mut self, + store: &BS, + deal_id: DealID, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let mut deal_proposals = DealArray::load(&self.proposals, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposal array")?; + + let proposal = deal_proposals + .delete(deal_id) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("no such deal proposal {}", deal_id) + })?; -pub(super) struct MarketStateMutation<'bs, 's, BS> { - pub(super) st: &'s mut State, - pub(super) store: &'bs BS, + self.proposals = deal_proposals + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal proposals")?; - pub(super) proposal_permit: Permission, - pub(super) deal_proposals: Option>, + Ok(proposal) + } - pub(super) state_permit: Permission, - pub(super) deal_states: Option>, + pub fn put_deal_proposals( + &mut self, + store: &BS, + new_deal_proposals: &[(DealID, DealProposal)], + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut deal_proposals = DealArray::load(&self.proposals, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposal array")?; + + new_deal_proposals.iter().try_for_each(|(id, proposal)| -> Result<(), ActorError> { + deal_proposals + .set(*id, proposal.clone()) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal proposal")?; + Ok(()) + })?; - pub(super) escrow_permit: Permission, - pub(super) escrow_table: Option>, + self.proposals = deal_proposals + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal proposals")?; - pub(super) pending_permit: Permission, - pub(super) pending_deals: Option>, - pub(super) pending_deal_allocation_ids: Option>, + Ok(()) + } - pub(super) dpe_permit: Permission, - pub(super) deals_by_epoch: Option>, + pub fn put_pending_deal_allocation_ids( + &mut self, + store: &BS, + new_pending_deal_allocation_ids: &[(BytesKey, AllocationID)], + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut pending_deal_allocation_ids = make_map_with_root_and_bitwidth( + &self.pending_deal_allocation_ids, + store, + HAMT_BIT_WIDTH, + ) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load pending deal allocation id's")?; - pub(super) locked_permit: Permission, - pub(super) locked_table: Option>, - pub(super) total_client_locked_collateral: Option, - pub(super) total_provider_locked_collateral: Option, - pub(super) total_client_storage_fee: Option, + new_pending_deal_allocation_ids.iter().try_for_each( + |(deal_id, allocation_id)| -> Result<(), ActorError> { + pending_deal_allocation_ids.set(deal_id.clone(), *allocation_id).context_code( + ExitCode::USR_ILLEGAL_STATE, + "failed to set pending deal allocation id", + )?; + Ok(()) + }, + )?; - pub(super) next_deal_id: DealID, -} + self.pending_deal_allocation_ids = pending_deal_allocation_ids.flush().context_code( + ExitCode::USR_ILLEGAL_STATE, + "failed to flush pending deal allocation id", + )?; -impl<'bs, 's, BS> MarketStateMutation<'bs, 's, BS> -where - BS: Blockstore, -{ - pub(super) fn new(st: &'s mut State, store: &'bs BS) -> Self { - Self { - next_deal_id: st.next_id, - st, - store, - proposal_permit: Permission::Invalid, - deal_proposals: None, - state_permit: Permission::Invalid, - deal_states: None, - escrow_permit: Permission::Invalid, - escrow_table: None, - pending_permit: Permission::Invalid, - pending_deals: None, - pending_deal_allocation_ids: None, - dpe_permit: Permission::Invalid, - deals_by_epoch: None, - locked_permit: Permission::Invalid, - locked_table: None, - total_client_locked_collateral: None, - total_provider_locked_collateral: None, - total_client_storage_fee: None, - } + Ok(()) } - pub(super) fn build(&mut self) -> anyhow::Result<&mut Self> { - if self.proposal_permit != Permission::Invalid { - self.deal_proposals = Some(DealArray::load(&self.st.proposals, self.store)?); - } + pub fn remove_pending_deal_allocation_id( + &mut self, + store: &BS, + deal_id_key: &BytesKey, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let mut pending_deal_allocation_ids = make_map_with_root_and_bitwidth( + &self.pending_deal_allocation_ids, + store, + HAMT_BIT_WIDTH, + ) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load pending deal allocation id's")?; + + let rval_allocation_id = pending_deal_allocation_ids + .delete(deal_id_key) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("no such deal proposal {:#?}", deal_id_key) + })?; - if self.state_permit != Permission::Invalid { - self.deal_states = Some(DealMetaArray::load(&self.st.states, self.store)?); - } + self.pending_deal_allocation_ids = pending_deal_allocation_ids + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal proposals")?; - if self.locked_permit != Permission::Invalid { - self.locked_table = Some(BalanceTable::from_root(self.store, &self.st.locked_table)?); - self.total_client_locked_collateral = - Some(self.st.total_client_locked_collateral.clone()); - self.total_client_storage_fee = Some(self.st.total_client_storage_fee.clone()); - self.total_provider_locked_collateral = - Some(self.st.total_provider_locked_collateral.clone()); - } + Ok(rval_allocation_id) + } - if self.escrow_permit != Permission::Invalid { - self.escrow_table = Some(BalanceTable::from_root(self.store, &self.st.escrow_table)?); - } + pub fn put_deals_by_epoch( + &mut self, + store: &BS, + new_deals_by_epoch: &[(ChainEpoch, DealID)], + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut deals_by_epoch = SetMultimap::from_root(store, &self.deal_ops_by_epoch) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deals by epoch")?; + + new_deals_by_epoch.iter().try_for_each(|(epoch, id)| -> Result<(), ActorError> { + deals_by_epoch + .put(*epoch, *id) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal")?; + Ok(()) + })?; - if self.pending_permit != Permission::Invalid { - self.pending_deals = Some(Set::from_root(self.store, &self.st.pending_proposals)?); - self.pending_deal_allocation_ids = Some(make_map_with_root_and_bitwidth( - &self.st.pending_deal_allocation_ids, - self.store, - HAMT_BIT_WIDTH, - )?); - } + self.deal_ops_by_epoch = deals_by_epoch + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deals by epoch")?; - if self.dpe_permit != Permission::Invalid { - self.deals_by_epoch = - Some(SetMultimap::from_root(self.store, &self.st.deal_ops_by_epoch)?); - } + Ok(()) + } - self.next_deal_id = self.st.next_id; + pub fn put_batch_deals_by_epoch( + &mut self, + store: &BS, + new_deals_by_epoch: &BTreeMap>, + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut deals_by_epoch = SetMultimap::from_root(store, &self.deal_ops_by_epoch) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deals by epoch")?; + + new_deals_by_epoch.iter().try_for_each(|(epoch, deals)| -> Result<(), ActorError> { + deals_by_epoch + .put_many(*epoch, deals) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to reinsert deal IDs for epoch {}", epoch) + })?; + Ok(()) + })?; - Ok(self) - } + self.deal_ops_by_epoch = deals_by_epoch + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deals by epoch")?; - pub(super) fn with_deal_proposals(&mut self, permit: Permission) -> &mut Self { - self.proposal_permit = permit; - self + Ok(()) } - pub(super) fn with_deal_states(&mut self, permit: Permission) -> &mut Self { - self.state_permit = permit; - self + pub fn get_deals_for_epoch( + &self, + store: &BS, + key: ChainEpoch, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let mut deal_ids = Vec::new(); + + let deals_by_epoch = SetMultimap::from_root(store, &self.deal_ops_by_epoch) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deals by epoch")?; + + deals_by_epoch + .for_each(key, |deal_id| { + deal_ids.push(deal_id); + Ok(()) + }) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal state")?; + + Ok(deal_ids) } - pub(super) fn with_escrow_table(&mut self, permit: Permission) -> &mut Self { - self.escrow_permit = permit; - self - } + pub fn remove_deals_by_epoch( + &mut self, + store: &BS, + epochs_to_remove: &[ChainEpoch], + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut deals_by_epoch = SetMultimap::from_root(store, &self.deal_ops_by_epoch) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deals by epoch")?; + + epochs_to_remove.iter().try_for_each(|epoch| -> Result<(), ActorError> { + deals_by_epoch + .remove_all(*epoch) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to delete deal ops for epoch {}", epoch) + })?; + Ok(()) + })?; - pub(super) fn with_locked_table(&mut self, permit: Permission) -> &mut Self { - self.locked_permit = permit; - self - } + self.deal_ops_by_epoch = deals_by_epoch + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deals by epoch")?; - pub(super) fn with_pending_proposals(&mut self, permit: Permission) -> &mut Self { - self.pending_permit = permit; - self + Ok(()) } - pub(super) fn with_deals_by_epoch(&mut self, permit: Permission) -> &mut Self { - self.dpe_permit = permit; - self + pub fn add_balance_to_escrow_table( + &mut self, + store: &BS, + addr: &Address, + amount: &TokenAmount, + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut escrow_table = BalanceTable::from_root(store, &self.escrow_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load escrow table")?; + + escrow_table + .add(addr, amount) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to add escrow table")?; + + self.escrow_table = escrow_table + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; + + Ok(()) } - pub(super) fn commit_state(&mut self) -> Result<(), ActorError> { - if self.proposal_permit == Permission::Write { - if let Some(s) = &mut self.deal_proposals { - self.st.proposals = s - .flush() - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal proposals")?; - } - } + pub fn withdraw_balance_from_escrow_table( + &mut self, + store: &BS, + addr: &Address, + amount: &TokenAmount, + ) -> Result + where + BS: Blockstore, + { + let mut escrow_table = BalanceTable::from_root(store, &self.escrow_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load escrow table")?; - if self.state_permit == Permission::Write { - if let Some(s) = &mut self.deal_states { - self.st.states = s - .flush() - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal states")?; - } - } + let locked_table = BalanceTable::from_root(store, &self.locked_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load locked table")?; - if self.locked_permit == Permission::Write { - if let Some(s) = &mut self.locked_table { - self.st.locked_table = s - .root() - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush locked table")?; - } - if let Some(s) = &mut self.total_client_locked_collateral { - self.st.total_client_locked_collateral = s.clone(); - } - if let Some(s) = &mut self.total_provider_locked_collateral { - self.st.total_provider_locked_collateral = s.clone(); - } - if let Some(s) = &mut self.total_client_storage_fee { - self.st.total_client_storage_fee = s.clone(); - } - } + let min_balance = locked_table + .get(addr) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get locked balance")?; - if self.escrow_permit == Permission::Write { - if let Some(s) = &mut self.escrow_table { - self.st.escrow_table = s - .root() - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; - } - } + let ex = escrow_table + .subtract_with_minimum(addr, amount, &min_balance) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to subtract from escrow table")?; - if self.pending_permit == Permission::Write { - if let Some(s) = &mut self.pending_deals { - self.st.pending_proposals = s - .root() - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; - } - if let Some(s) = &mut self.pending_deal_allocation_ids { - self.st.pending_deal_allocation_ids = s.flush().context_code( - ExitCode::USR_ILLEGAL_STATE, - "failed to flush pending deal allocation ids", - )?; - } - } + self.escrow_table = escrow_table + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; - if self.dpe_permit == Permission::Write { - if let Some(s) = &mut self.deals_by_epoch { - self.st.deal_ops_by_epoch = s - .root() - .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; - } - } + Ok(ex) + } + + pub fn has_pending_deal(&self, store: &BS, key: Cid) -> Result + where + BS: Blockstore, + { + let pending_deals = Set::from_root(store, &self.pending_proposals) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get pending deals")?; - self.st.next_id = self.next_deal_id; + let rval = pending_deals + .has(&key.to_bytes()) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get pending deals")?; + + Ok(rval) + } + + pub fn put_pending_deals( + &mut self, + store: &BS, + new_pending_deals: &[Cid], + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + let mut pending_deals = Set::from_root(store, &self.pending_proposals) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load pending deals")?; + + new_pending_deals.iter().try_for_each(|key: &Cid| -> Result<(), ActorError> { + pending_deals + .put(key.to_bytes().into()) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal")?; + Ok(()) + })?; + + self.pending_proposals = pending_deals + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush pending deals")?; Ok(()) } + pub fn remove_pending_deal( + &mut self, + store: &BS, + pending_deal_key: Cid, + ) -> Result, ActorError> + where + BS: Blockstore, + { + let mut pending_deals = Set::from_root(store, &self.pending_proposals) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load pending deals")?; + + let rval_pending_deal = pending_deals + .delete(&pending_deal_key.to_bytes()) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to delete pending proposal {}", pending_deal_key) + })?; + + self.pending_proposals = pending_deals + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush pending deals")?; + + Ok(rval_pending_deal) + } + //////////////////////////////////////////////////////////////////////////////// // Deal state operations //////////////////////////////////////////////////////////////////////////////// #[allow(clippy::too_many_arguments)] - pub(super) fn update_pending_deal_state( + pub fn put_pending_deal_state( &mut self, + store: &BS, policy: &Policy, state: &DealState, deal: &DealProposal, epoch: ChainEpoch, - ) -> Result<(TokenAmount, ChainEpoch, bool), ActorError> { + ) -> Result<(TokenAmount, ChainEpoch, bool), ActorError> + where + BS: Blockstore, + { let ever_updated = state.last_updated_epoch != EPOCH_UNDEFINED; let ever_slashed = state.slash_epoch != EPOCH_UNDEFINED; @@ -423,7 +632,7 @@ where let total_payment = &deal.storage_price_per_epoch * num_epochs_elapsed; if total_payment.is_positive() { - self.transfer_balance(&deal.client, &deal.provider, &total_payment)?; + self.transfer_balance(store, &deal.client, &deal.provider, &total_payment)?; } if ever_slashed { @@ -431,33 +640,31 @@ where let payment_remaining = deal_get_payment_remaining(deal, state.slash_epoch)?; // Unlock remaining storage fee - self.unlock_balance(&deal.client, &payment_remaining, Reason::ClientStorageFee) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to unlock remaining client storage fee", - ) - })?; + self.unlock_balance(store, &deal.client, &payment_remaining, Reason::ClientStorageFee) + .context_code( + ExitCode::USR_ILLEGAL_STATE, + "failed to unlock remaining client storage fee", + )?; // Unlock client collateral - self.unlock_balance(&deal.client, &deal.client_collateral, Reason::ClientCollateral) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to unlock client collateral", - ) - })?; + self.unlock_balance( + store, + &deal.client, + &deal.client_collateral, + Reason::ClientCollateral, + ) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to unlock client collateral")?; // slash provider collateral let slashed = deal.provider_collateral.clone(); - self.slash_balance(&deal.provider, &slashed, Reason::ProviderCollateral) - .map_err(|e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "slashing balance"))?; + self.slash_balance(store, &deal.provider, &slashed, Reason::ProviderCollateral) + .context_code(ExitCode::USR_ILLEGAL_STATE, "slashing balance")?; return Ok((slashed, EPOCH_UNDEFINED, true)); } if epoch >= deal.end_epoch { - self.process_deal_expired(deal, state)?; + self.process_deal_expired(store, deal, state)?; return Ok((TokenAmount::zero(), EPOCH_UNDEFINED, true)); } @@ -472,111 +679,125 @@ where /// Deal start deadline elapsed without appearing in a proven sector. /// Slash a portion of provider's collateral, and unlock remaining collaterals /// for both provider and client. - pub(super) fn process_deal_init_timed_out( + pub fn process_deal_init_timed_out( &mut self, + store: &BS, deal: &DealProposal, - ) -> Result { - self.unlock_balance(&deal.client, &deal.total_storage_fee(), Reason::ClientStorageFee) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failure unlocking client storage fee", - ) - })?; + ) -> Result + where + BS: Blockstore, + { + self.unlock_balance( + store, + &deal.client, + &deal.total_storage_fee(), + Reason::ClientStorageFee, + ) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failure unlocking client storage fee")?; - self.unlock_balance(&deal.client, &deal.client_collateral, Reason::ClientCollateral) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failure unlocking client collateral", - ) - })?; + self.unlock_balance(store, &deal.client, &deal.client_collateral, Reason::ClientCollateral) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failure unlocking client collateral")?; let amount_slashed = collateral_penalty_for_deal_activation_missed(deal.provider_collateral.clone()); let amount_remaining = deal.provider_balance_requirement() - &amount_slashed; - self.slash_balance(&deal.provider, &amount_slashed, Reason::ProviderCollateral).map_err( - |e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to slash balance"), - )?; + self.slash_balance(store, &deal.provider, &amount_slashed, Reason::ProviderCollateral) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to slash balance")?; - self.unlock_balance(&deal.provider, &amount_remaining, Reason::ProviderCollateral) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to unlock deal provider balance", - ) - })?; + self.unlock_balance(store, &deal.provider, &amount_remaining, Reason::ProviderCollateral) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to unlock deal provider balance")?; Ok(amount_slashed) } /// Normal expiration. Unlock collaterals for both miner and client. - fn process_deal_expired( + fn process_deal_expired( &mut self, + store: &BS, deal: &DealProposal, state: &DealState, - ) -> Result<(), ActorError> { + ) -> Result<(), ActorError> + where + BS: Blockstore, + { if state.sector_start_epoch == EPOCH_UNDEFINED { return Err(actor_error!(illegal_state, "start sector epoch undefined")); } - self.unlock_balance(&deal.provider, &deal.provider_collateral, Reason::ProviderCollateral) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed unlocking deal provider balance", - ) - })?; + self.unlock_balance( + store, + &deal.provider, + &deal.provider_collateral, + Reason::ProviderCollateral, + ) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed unlocking deal provider balance")?; - self.unlock_balance(&deal.client, &deal.client_collateral, Reason::ClientCollateral) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed unlocking deal client balance", - ) - })?; + self.unlock_balance(store, &deal.client, &deal.client_collateral, Reason::ClientCollateral) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed unlocking deal client balance")?; Ok(()) } - pub(super) fn generate_storage_deal_id(&mut self) -> DealID { - let ret = self.next_deal_id; - self.next_deal_id += 1; + pub fn generate_storage_deal_id(&mut self) -> DealID { + let ret = self.next_id; + self.next_id += 1; ret } // Return true when the funds in escrow for the input address can cover an additional lockup of amountToLock - pub(super) fn balance_covered( + pub fn balance_covered( &self, + store: &BS, addr: Address, amount_to_lock: &TokenAmount, - ) -> anyhow::Result { - let prev_locked = self.locked_table.as_ref().unwrap().get(&addr).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get locked balance") - })?; - let escrow_balance = self.escrow_table.as_ref().unwrap().get(&addr).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get escrow balance") - })?; + ) -> Result + where + BS: Blockstore, + { + let escrow_table = BalanceTable::from_root(store, &self.escrow_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load escrow table")?; + + let locked_table = BalanceTable::from_root(store, &self.locked_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load locked table")?; + + let escrow_balance = escrow_table + .get(&addr) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get escrow balance")?; + + let prev_locked = locked_table + .get(&addr) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get locked balance")?; + Ok((prev_locked + amount_to_lock) <= escrow_balance) } - pub(super) fn maybe_lock_balance( + fn maybe_lock_balance( &mut self, + store: &BS, addr: &Address, amount: &TokenAmount, - ) -> Result<(), ActorError> { + ) -> Result<(), ActorError> + where + BS: Blockstore, + { if amount.is_negative() { return Err(actor_error!(illegal_state, "cannot lock negative amount {}", amount)); } - let prev_locked = self.locked_table.as_ref().unwrap().get(addr).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get locked balance") - })?; + let escrow_table = BalanceTable::from_root(store, &self.escrow_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load escrow table")?; - let escrow_balance = self.escrow_table.as_ref().unwrap().get(addr).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to get escrow balance") - })?; + let mut locked_table = BalanceTable::from_root(store, &self.locked_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load locked table")?; + + let prev_locked = locked_table + .get(addr) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get locked balance")?; + + let escrow_balance = escrow_table + .get(addr) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get escrow balance")?; if &prev_locked + amount > escrow_balance { return Err(actor_error!(insufficient_funds; @@ -585,103 +806,172 @@ where addr, escrow_balance, prev_locked, amount)); } - self.locked_table.as_mut().unwrap().add(addr, amount).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to add locked balance") - })?; + locked_table + .add(addr, amount) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to add locked balance")?; + + self.locked_table = locked_table + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush locked table")?; + Ok(()) } - pub(super) fn lock_client_and_provider_balances( + pub fn lock_client_and_provider_balances( &mut self, + store: &BS, proposal: &DealProposal, - ) -> Result<(), ActorError> { - self.maybe_lock_balance(&proposal.client, &proposal.client_balance_requirement()) - .map_err(|e| e.wrap("failed to lock client funds"))?; + ) -> Result<(), ActorError> + where + BS: Blockstore, + { + self.maybe_lock_balance(store, &proposal.client, &proposal.client_balance_requirement()) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to lock client funds")?; - self.maybe_lock_balance(&proposal.provider, &proposal.provider_collateral) - .map_err(|e| e.wrap("failed to lock provider funds"))?; + self.maybe_lock_balance(store, &proposal.provider, &proposal.provider_collateral) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to lock provider funds")?; + + self.total_client_locked_collateral += &proposal.client_collateral; + + self.total_client_storage_fee += proposal.total_storage_fee(); + + self.total_provider_locked_collateral += &proposal.provider_collateral; - if let Some(v) = self.total_client_locked_collateral.as_mut() { - *v += &proposal.client_collateral; - } - if let Some(v) = self.total_client_storage_fee.as_mut() { - *v += proposal.total_storage_fee(); - } - if let Some(v) = self.total_provider_locked_collateral.as_mut() { - *v += &proposal.provider_collateral; - } Ok(()) } - fn unlock_balance( + fn unlock_balance( &mut self, + store: &BS, addr: &Address, amount: &TokenAmount, lock_reason: Reason, - ) -> anyhow::Result<()> { + ) -> Result<(), ActorError> + where + BS: Blockstore, + { if amount.is_negative() { - return Err(actor_error!(illegal_state, "unlock negative amount: {}", amount).into()); + return Err(actor_error!(illegal_state, "unlock negative amount: {}", amount)); } - self.locked_table.as_mut().unwrap().must_subtract(addr, amount)?; + + let mut locked_table = BalanceTable::from_root(store, &self.locked_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load locked table")?; + + locked_table + .must_subtract(addr, amount) + .context_code(ExitCode::USR_ILLEGAL_STATE, "subtract from locked table failed")?; match lock_reason { - Reason::ClientCollateral => self.total_client_locked_collateral.as_mut().map(|v| { - *v -= amount; - }), - Reason::ClientStorageFee => self.total_client_storage_fee.as_mut().map(|v| { - *v -= amount; - }), - Reason::ProviderCollateral => self.total_provider_locked_collateral.as_mut().map(|v| { - *v -= amount; - }), + Reason::ClientCollateral => { + self.total_client_locked_collateral -= amount; + } + Reason::ClientStorageFee => { + self.total_client_storage_fee -= amount; + } + Reason::ProviderCollateral => { + self.total_provider_locked_collateral -= amount; + } }; + self.locked_table = locked_table + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush locked table")?; + Ok(()) } /// move funds from locked in client to available in provider - fn transfer_balance( + fn transfer_balance( &mut self, + store: &BS, from_addr: &Address, to_addr: &Address, amount: &TokenAmount, - ) -> Result<(), ActorError> { + ) -> Result<(), ActorError> + where + BS: Blockstore, + { if amount.is_negative() { return Err(actor_error!(illegal_state, "transfer negative amount: {}", amount)); } + let mut escrow_table = BalanceTable::from_root(store, &self.escrow_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load escrow table")?; + // Subtract from locked and escrow tables - self.escrow_table - .as_mut() - .unwrap() + escrow_table .must_subtract(from_addr, amount) - .map_err(|e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "subtract from escrow"))?; + .context_code(ExitCode::USR_ILLEGAL_STATE, "subtract from escrow")?; - self.unlock_balance(from_addr, amount, Reason::ClientStorageFee) - .map_err(|e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "subtract from locked"))?; + self.unlock_balance(store, from_addr, amount, Reason::ClientStorageFee) + .context_code(ExitCode::USR_ILLEGAL_STATE, "subtract from locked")?; // Add subtracted amount to the recipient - self.escrow_table - .as_mut() - .unwrap() + escrow_table .add(to_addr, amount) - .map_err(|e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "add to escrow"))?; + .context_code(ExitCode::USR_ILLEGAL_STATE, "add to escrow")?; + + self.escrow_table = escrow_table + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; Ok(()) } - fn slash_balance( + fn slash_balance( &mut self, + store: &BS, addr: &Address, amount: &TokenAmount, lock_reason: Reason, - ) -> anyhow::Result<()> { + ) -> Result<(), ActorError> + where + BS: Blockstore, + { if amount.is_negative() { - return Err(actor_error!(illegal_state, "negative amount to slash: {}", amount).into()); + return Err(actor_error!(illegal_state, "negative amount to slash: {}", amount)); } + let mut escrow_table = BalanceTable::from_root(store, &self.escrow_table) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load escrow table")?; + // Subtract from locked and escrow tables - self.escrow_table.as_mut().unwrap().must_subtract(addr, amount)?; - self.unlock_balance(addr, amount, lock_reason) + escrow_table + .must_subtract(addr, amount) + .context_code(ExitCode::USR_ILLEGAL_STATE, "subtract from escrow failed")?; + + self.escrow_table = escrow_table + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; + + self.unlock_balance(store, addr, amount, lock_reason) + } +} + +fn deal_get_payment_remaining( + deal: &DealProposal, + mut slash_epoch: ChainEpoch, +) -> Result { + if slash_epoch > deal.end_epoch { + return Err(actor_error!( + illegal_state, + "deal slash epoch {} after end epoch {}", + slash_epoch, + deal.end_epoch + )); } + + // Payments are always for start -> end epoch irrespective of when the deal is slashed. + slash_epoch = std::cmp::max(slash_epoch, deal.start_epoch); + + let duration_remaining = deal.end_epoch - slash_epoch; + if duration_remaining < 0 { + return Err(actor_error!( + illegal_state, + "deal remaining duration negative: {}", + duration_remaining + )); + } + + Ok(&deal.storage_price_per_epoch * duration_remaining as u64) }