diff --git a/actors/market/src/lib.rs b/actors/market/src/lib.rs index 83bab65964..9ba13ad193 100644 --- a/actors/market/src/lib.rs +++ b/actors/market/src/lib.rs @@ -59,6 +59,9 @@ fil_actors_runtime::wasm_trampoline!(Actor); pub const NO_ALLOCATION_ID: u64 = 0; +// An exit code indicating that information about a past deal is no longer available. +pub const EX_DEAL_EXPIRED: ExitCode = ExitCode::new(32); + /// Market actor methods available #[derive(FromPrimitive)] #[repr(u64)] @@ -85,6 +88,7 @@ pub enum Method { GetDealClientCollateralExported = frc42_dispatch::method_hash!("GetDealClientCollateral"), GetDealProviderCollateralExported = frc42_dispatch::method_hash!("GetDealProviderCollateral"), GetDealVerifiedExported = frc42_dispatch::method_hash!("GetDealVerified"), + GetDealActivationExported = frc42_dispatch::method_hash!("GetDealActivation"), } /// Market Actor @@ -1148,6 +1152,53 @@ impl Actor { let found = rt.state::()?.get_proposal(rt.store(), params.id)?; Ok(GetDealVerifiedReturn { verified: found.verified_deal }) } + + /// Fetches activation state for a deal. + /// This will be available from when the proposal is published until an undefined period after + /// the deal finishes (either normally or by termination). + /// Returns USR_NOT_FOUND if the deal doesn't exist (yet), or EX_DEAL_EXPIRED if the deal + /// has been removed from state. + fn get_deal_activation( + rt: &mut impl Runtime, + params: GetDealActivationParams, + ) -> Result { + rt.validate_immediate_caller_accept_any()?; + let st = rt.state::()?; + let found = st.find_deal_state(rt.store(), params.id)?; + match found { + Some(state) => Ok(GetDealActivationReturn { + // If we have state, the deal has been activated. + // It may also have completed normally, or been terminated, + // but not yet been cleaned up. + activated: state.sector_start_epoch, + terminated: state.slash_epoch, + }), + None => { + // State::get_proposal will fail with USR_NOT_FOUND in either case. + let maybe_proposal = st.find_proposal(rt.store(), params.id)?; + match maybe_proposal { + Some(_) => Ok(GetDealActivationReturn { + // The proposal has been published, but not activated. + activated: EPOCH_UNDEFINED, + terminated: EPOCH_UNDEFINED, + }), + None => { + if params.id < st.next_id { + // If the deal ID has been used, it must have been cleaned up. + Err(ActorError::unchecked( + EX_DEAL_EXPIRED, + format!("deal {} expired", params.id), + )) + } else { + // We can't distinguish between failing to activate, or having been + // cleaned up after completion/termination. + Err(ActorError::not_found(format!("no such deal {}", params.id))) + } + } + } + } + } + } } fn compute_data_commitment( @@ -1600,6 +1651,10 @@ impl ActorCode for Actor { let res = Self::get_deal_verified(rt, cbor::deserialize_params(params)?)?; Ok(RawBytes::serialize(res)?) } + Some(Method::GetDealActivationExported) => { + let res = Self::get_deal_activation(rt, cbor::deserialize_params(params)?)?; + Ok(RawBytes::serialize(res)?) + } None => Err(actor_error!(unhandled_message, "Invalid method")), } } diff --git a/actors/market/src/state.rs b/actors/market/src/state.rs index 66c7830667..bf3fea6562 100644 --- a/actors/market/src/state.rs +++ b/actors/market/src/state.rs @@ -120,15 +120,36 @@ impl State { 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) + } + + pub fn find_proposal( + &self, + store: &BS, + id: DealID, + ) -> Result, ActorError> { let proposals = DealArray::load(&self.proposals, store) .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal proposals")?; - let found = proposals - .get(id) - .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { - format!("failed to load deal proposal {}", id) - })? - .with_context_code(ExitCode::USR_NOT_FOUND, || format!("no such deal {}", id))?; - Ok(found.clone()) + let maybe = proposals.get(id).with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to load deal proposal {}", id) + })?; + Ok(maybe.cloned()) + } + + pub fn find_deal_state( + &self, + store: &BS, + id: DealID, + ) -> Result, ActorError> { + let states = DealMetaArray::load(&self.states, store) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal states")?; + let found = states.get(id).with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to load deal state {}", id) + })?; + Ok(found.cloned()) } pub(super) fn mutator<'bs, BS: Blockstore>( diff --git a/actors/market/src/types.rs b/actors/market/src/types.rs index 7c28f164b7..eaa98b9704 100644 --- a/actors/market/src/types.rs +++ b/actors/market/src/types.rs @@ -206,3 +206,13 @@ pub type GetDealVerifiedParams = DealQueryParams; pub struct GetDealVerifiedReturn { pub verified: bool, } + +pub type GetDealActivationParams = DealQueryParams; +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone, Eq, PartialEq)] +pub struct GetDealActivationReturn { + /// Epoch at which the deal was activated, or -1. + /// This may be before the proposed start epoch. + pub activated: ChainEpoch, + /// Epoch at which the deal was terminated abnormally, or -1. + pub terminated: ChainEpoch, +} diff --git a/actors/market/tests/deal_api_test.rs b/actors/market/tests/deal_api_test.rs index ca18ed9991..91584188b4 100644 --- a/actors/market/tests/deal_api_test.rs +++ b/actors/market/tests/deal_api_test.rs @@ -1,15 +1,22 @@ use fvm_ipld_encoding::RawBytes; use fvm_shared::clock::ChainEpoch; +use fvm_shared::error::ExitCode; +use fvm_shared::METHOD_SEND; use serde::de::DeserializeOwned; use fil_actor_market::{ - Actor as MarketActor, DealQueryParams, GetDealClientCollateralReturn, GetDealClientReturn, - GetDealDataCommitmentReturn, GetDealLabelReturn, GetDealProviderCollateralReturn, - GetDealProviderReturn, GetDealTermReturn, GetDealTotalPriceReturn, GetDealVerifiedReturn, - Method, + Actor as MarketActor, DealQueryParams, GetDealActivationReturn, GetDealClientCollateralReturn, + GetDealClientReturn, GetDealDataCommitmentReturn, GetDealLabelReturn, + GetDealProviderCollateralReturn, GetDealProviderReturn, GetDealTermReturn, + GetDealTotalPriceReturn, GetDealVerifiedReturn, Method, EX_DEAL_EXPIRED, }; use fil_actors_runtime::network::EPOCHS_IN_DAY; -use fil_actors_runtime::test_utils::{MockRuntime, ACCOUNT_ACTOR_CODE_ID}; +use fil_actors_runtime::runtime::policy_constants::DEAL_UPDATES_INTERVAL; +use fil_actors_runtime::test_utils::{ + expect_abort_contains_message, MockRuntime, ACCOUNT_ACTOR_CODE_ID, +}; +use fil_actors_runtime::ActorError; +use fil_actors_runtime::BURNT_FUNDS_ACTOR_ADDR; use harness::*; mod harness; @@ -71,11 +78,83 @@ fn proposal_data() { check_state(&rt); } +#[test] +fn activation() { + let start_epoch = 10; + let end_epoch = start_epoch + 180 * EPOCHS_IN_DAY; + let publish_epoch = ChainEpoch::from(1); + + let mut rt = setup(); + rt.set_epoch(publish_epoch); + let next_allocation_id = 1; + + let proposal = generate_deal_and_add_funds( + &mut rt, + CLIENT_ADDR, + &MinerAddresses::default(), + start_epoch, + end_epoch, + ); + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); + let id = + publish_deals(&mut rt, &MinerAddresses::default(), &[proposal.clone()], next_allocation_id) + [0]; + + let activation: GetDealActivationReturn = + query_deal(&mut rt, Method::GetDealActivationExported, id); + assert_eq!(-1, activation.activated); + assert_eq!(-1, activation.terminated); + + // activate the deal + let activate_epoch = start_epoch - 2; + rt.set_epoch(activate_epoch); + activate_deals(&mut rt, end_epoch + 1, PROVIDER_ADDR, activate_epoch, &[id]); + let activation: GetDealActivationReturn = + query_deal(&mut rt, Method::GetDealActivationExported, id); + assert_eq!(activate_epoch, activation.activated); + assert_eq!(-1, activation.terminated); + + // terminate early + let terminate_epoch = activate_epoch + 100; + rt.set_epoch(terminate_epoch); + terminate_deals(&mut rt, PROVIDER_ADDR, &[id]); + let activation: GetDealActivationReturn = + query_deal(&mut rt, Method::GetDealActivationExported, id); + assert_eq!(activate_epoch, activation.activated); + assert_eq!(terminate_epoch, activation.terminated); + + // Clean up state + let clean_epoch = terminate_epoch + DEAL_UPDATES_INTERVAL; + rt.set_epoch(clean_epoch); + rt.expect_send( + BURNT_FUNDS_ACTOR_ADDR, + METHOD_SEND, + RawBytes::default(), + proposal.provider_collateral, + RawBytes::default(), + ExitCode::OK, + ); + cron_tick(&mut rt); + expect_abort_contains_message( + EX_DEAL_EXPIRED, + "expired", + query_deal_raw(&mut rt, Method::GetDealActivationExported, id), + ); + + // Non-existent deal is NOT FOUND + expect_abort_contains_message( + ExitCode::USR_NOT_FOUND, + "no such deal", + query_deal_raw(&mut rt, Method::GetDealActivationExported, id + 1), + ); +} + fn query_deal(rt: &mut MockRuntime, method: Method, id: u64) -> T { + query_deal_raw(rt, method, id).unwrap().deserialize().unwrap() +} + +fn query_deal_raw(rt: &mut MockRuntime, method: Method, id: u64) -> Result { let params = DealQueryParams { id }; rt.expect_validate_caller_any(); rt.call::(method as u64, &RawBytes::serialize(params).unwrap()) - .unwrap() - .deserialize() - .unwrap() } diff --git a/runtime/src/actor_error.rs b/runtime/src/actor_error.rs index d67cd07181..bc851a4a05 100644 --- a/runtime/src/actor_error.rs +++ b/runtime/src/actor_error.rs @@ -9,7 +9,7 @@ use thiserror::Error; #[error("ActorError(exit_code: {exit_code:?}, msg: {msg})")] pub struct ActorError { /// The exit code for this invocation. - /// Codes less than `FIRST_ACTOR_EXIT_CODE` are prohibited and will be overwritten by the VM. + /// Codes less than `FIRST_USER_EXIT_CODE` are prohibited and will be overwritten by the VM. exit_code: ExitCode, /// Message for debugging purposes, msg: String,