From 968d4c0e91cbd46fb8afaba02b8e15ae23fa0d86 Mon Sep 17 00:00:00 2001 From: Alex <445306+anorth@users.noreply.github.com> Date: Fri, 16 Sep 2022 09:59:10 +1000 Subject: [PATCH] Market actor transfers datacap and makes verified allocations for deals, instead of UseBytes (#657) --- Cargo.lock | 3 + actors/market/Cargo.toml | 19 +- actors/market/src/deal.rs | 3 + actors/market/src/ext.rs | 36 +- actors/market/src/lib.rs | 430 ++++++++++-------- actors/market/src/state.rs | 58 ++- actors/market/src/testing.rs | 25 +- .../market/tests/cron_tick_timedout_deals.rs | 36 +- actors/market/tests/harness.rs | 93 ++-- actors/market/tests/market_actor_test.rs | 146 ++++-- .../tests/on_miner_sectors_terminate.rs | 2 +- .../tests/verify_deals_for_activation_test.rs | 3 +- actors/verifreg/src/lib.rs | 14 +- actors/verifreg/src/state.rs | 5 +- actors/verifreg/src/types.rs | 8 + actors/verifreg/tests/harness/mod.rs | 14 +- actors/verifreg/tests/verifreg_actor_test.rs | 22 +- runtime/src/runtime/policy.rs | 10 +- test_vm/Cargo.toml | 28 +- test_vm/src/util.rs | 62 ++- test_vm/tests/terminate_test.rs | 34 +- 21 files changed, 682 insertions(+), 369 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b7223296..b7c222df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,12 +635,14 @@ dependencies = [ "fil_actor_reward", "fil_actor_verifreg", "fil_actors_runtime", + "fil_fungible_token", "fvm_ipld_amt", "fvm_ipld_bitfield", "fvm_ipld_blockstore", "fvm_ipld_encoding", "fvm_ipld_hamt", "fvm_shared", + "integer-encoding", "itertools", "libipld-core", "log", @@ -1863,6 +1865,7 @@ dependencies = [ "fil_actor_verifreg", "fil_actors_runtime", "fil_builtin_actors_state", + "fil_fungible_token", "fvm_ipld_bitfield", "fvm_ipld_blockstore", "fvm_ipld_encoding", diff --git a/actors/market/Cargo.toml b/actors/market/Cargo.toml index 14aa9150f..57dd1500c 100644 --- a/actors/market/Cargo.toml +++ b/actors/market/Cargo.toml @@ -15,18 +15,21 @@ crate-type = ["cdylib", "lib"] [dependencies] fil_actors_runtime = { version = "9.0.0-alpha.1", path = "../../runtime", features = ["fil-actor"] } -fvm_ipld_hamt = "0.5.1" -fvm_shared = { version = "2.0.0-alpha.2", default-features = false } -fvm_ipld_bitfield = "0.5.2" -num-traits = "0.2.14" -num-derive = "0.3.3" -serde = { version = "1.0.136", features = ["derive"] } -cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } -log = "0.4.14" + anyhow = "1.0.56" +cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } +fil_fungible_token = { git = "https://github.com/helix-onchain/filecoin", rev = "39320534b8e992b9c5e53e15641ea4c9c113ed3e" } +fvm_ipld_bitfield = "0.5.2" fvm_ipld_blockstore = "0.1.1" fvm_ipld_encoding = "0.2.2" +fvm_ipld_hamt = "0.5.1" +fvm_shared = { version = "2.0.0-alpha.2", default-features = false } +integer-encoding = { version = "3.0.3", default-features = false } libipld-core = { version = "0.13.1", features = ["serde-codec"] } +log = "0.4.14" +num-derive = "0.3.3" +num-traits = "0.2.14" +serde = { version = "1.0.136", features = ["derive"] } [dev-dependencies] fil_actors_runtime = { path = "../../runtime", features = ["test_utils", "sector-default"] } diff --git a/actors/market/src/deal.rs b/actors/market/src/deal.rs index 6f7109414..f1f186aa3 100644 --- a/actors/market/src/deal.rs +++ b/actors/market/src/deal.rs @@ -1,6 +1,7 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use crate::ext::verifreg::AllocationID; use cid::{Cid, Version}; use fvm_ipld_encoding::tuple::*; use fvm_ipld_encoding::{BytesSer, Cbor}; @@ -146,4 +147,6 @@ pub struct DealState { pub last_updated_epoch: ChainEpoch, // -1 if deal never slashed pub slash_epoch: ChainEpoch, + // ID of the verified registry allocation/claim for this deal's data (0 if none). + pub verified_claim: AllocationID, } diff --git a/actors/market/src/ext.rs b/actors/market/src/ext.rs index 1ecd1ad2d..b4e8dcc0f 100644 --- a/actors/market/src/ext.rs +++ b/actors/market/src/ext.rs @@ -36,24 +36,38 @@ pub mod miner { pub mod verifreg { use super::*; + use cid::Cid; + use fvm_shared::clock::ChainEpoch; + use fvm_shared::piece::PaddedPieceSize; - // based on fil_actor_verifreg - pub const USE_BYTES_METHOD: u64 = 5; - pub const RESTORE_BYTES_METHOD: u64 = 6; + pub type AllocationID = u64; + pub type ClaimID = u64; - pub type UseBytesParams = BytesParams; - pub type RestoreBytesParams = BytesParams; + #[derive(Clone, Debug, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)] + pub struct AllocationRequest { + pub provider: Address, + pub data: Cid, + pub size: PaddedPieceSize, + pub term_min: ChainEpoch, + pub term_max: ChainEpoch, + pub expiration: ChainEpoch, + } #[derive(Clone, Debug, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)] - pub struct BytesParams { - /// Address of verified client. - pub address: Address, - /// Number of bytes to use. - #[serde(with = "bigint_ser")] - pub deal_size: StoragePower, + pub struct AllocationsRequest { + pub requests: Vec, + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)] + pub struct AllocationsResponse { + pub allocations: Vec, } } +pub mod datacap { + pub const TRANSFER_FROM_METHOD: u64 = 15; +} + pub mod reward { pub const THIS_EPOCH_REWARD_METHOD: u64 = 3; } diff --git a/actors/market/src/lib.rs b/actors/market/src/lib.rs index 34e267ff0..aeccdfa17 100644 --- a/actors/market/src/lib.rs +++ b/actors/market/src/lib.rs @@ -3,11 +3,14 @@ use cid::multihash::{Code, MultihashDigest, MultihashGeneric}; use cid::Cid; +use fil_fungible_token::token::types::{TransferFromParams, TransferFromReturn}; +use std::cmp::min; use std::collections::{BTreeMap, BTreeSet}; use fvm_ipld_bitfield::BitField; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::{Cbor, RawBytes}; +use fvm_ipld_hamt::BytesKey; use fvm_shared::address::Address; use fvm_shared::bigint::BigInt; use fvm_shared::clock::{ChainEpoch, QuantSpec, EPOCH_UNDEFINED}; @@ -18,21 +21,21 @@ use fvm_shared::piece::PieceInfo; use fvm_shared::reward::ThisEpochRewardReturn; use fvm_shared::sector::{RegisteredSealProof, SectorSize, StoragePower}; use fvm_shared::{ActorID, MethodNum, METHOD_CONSTRUCTOR, METHOD_SEND}; +use integer_encoding::VarInt; use log::info; use num_derive::FromPrimitive; use num_traits::{FromPrimitive, Zero}; -use fil_actors_runtime::cbor::serialize_vec; +use crate::ext::verifreg::{AllocationID, AllocationRequest}; +use fil_actors_runtime::cbor::{deserialize, serialize, serialize_vec}; use fil_actors_runtime::runtime::builtins::Type; use fil_actors_runtime::runtime::{ActorCode, Policy, Runtime}; use fil_actors_runtime::{ - actor_error, cbor, ActorDowncast, ActorError, BURNT_FUNDS_ACTOR_ADDR, CALLER_TYPES_SIGNABLE, - CRON_ACTOR_ADDR, REWARD_ACTOR_ADDR, STORAGE_POWER_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, - VERIFIED_REGISTRY_ACTOR_ADDR, + actor_error, cbor, ActorContext, ActorDowncast, ActorError, AsActorError, + BURNT_FUNDS_ACTOR_ADDR, CALLER_TYPES_SIGNABLE, CRON_ACTOR_ADDR, DATACAP_TOKEN_ACTOR_ADDR, + REWARD_ACTOR_ADDR, STORAGE_POWER_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, VERIFIED_REGISTRY_ACTOR_ADDR, }; -use crate::ext::verifreg::UseBytesParams; - pub use self::deal::*; use self::policy::*; pub use self::state::*; @@ -52,26 +55,7 @@ mod types; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(Actor); -fn request_miner_control_addrs( - rt: &mut RT, - miner_id: ActorID, -) -> Result<(Address, Address, Vec
), ActorError> -where - BS: Blockstore, - RT: Runtime, -{ - let ret = rt.send( - &Address::new_id(miner_id), - ext::miner::CONTROL_ADDRESSES_METHOD, - RawBytes::default(), - TokenAmount::zero(), - )?; - let addrs: ext::miner::GetControlAddressesReturnParams = ret.deserialize()?; - - Ok((addrs.owner, addrs.worker, addrs.control_addresses)) -} - -// * Updated to specs-actors commit: e195950ba98adb8ce362030356bf4a3809b7ec77 (v2.3.2) +pub const NO_ALLOCATION_ID: u64 = 0; /// Market actor methods available #[derive(FromPrimitive)] @@ -143,9 +127,7 @@ impl Actor { ) })?; - msm.commit_state().map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to flush state") - })?; + msm.commit_state()?; Ok(()) })?; @@ -200,9 +182,7 @@ impl Actor { ) })?; - msm.commit_state().map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to flush state") - })?; + msm.commit_state()?; Ok(ex) })?; @@ -266,16 +246,23 @@ impl Actor { let baseline_power = request_current_baseline_power(rt)?; let (network_raw_power, _) = request_current_network_power(rt)?; - // Drop invalid deals + struct ValidDeal { + proposal: DealProposal, + cid: Cid, + allocation: AllocationID, + } + + // Deals that passed validation. + let mut valid_deals: Vec = Vec::with_capacity(params.deals.len()); + // CIDs of valid proposals. let mut proposal_cid_lookup = BTreeSet::new(); - let mut valid_proposal_cids = Vec::new(); - let mut valid_deals = Vec::with_capacity(params.deals.len()); let mut total_client_lockup: BTreeMap = BTreeMap::new(); let mut total_provider_lockup = TokenAmount::zero(); let mut valid_input_bf = BitField::default(); - let mut state: State = rt.state::()?; + 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) @@ -348,7 +335,6 @@ impl Actor { // drop duplicate deals // Normalise provider and client addresses in the proposal stored on chain. // Must happen after signature verification and before taking cid. - deal.proposal.provider = Address::new_id(provider_id); deal.proposal.client = Address::new_id(client_id); let pcid = rt_deal_cid(rt, &deal.proposal).map_err( @@ -370,40 +356,62 @@ impl Actor { continue; } - // check VerifiedClient allowed cap and deduct PieceSize from cap - // drop deals with a DealSize that cannot be fully covered by VerifiedClient's available DataCap - if deal.proposal.verified_deal { - if let Err(e) = rt.send( - &VERIFIED_REGISTRY_ACTOR_ADDR, - crate::ext::verifreg::USE_BYTES_METHOD as u64, - RawBytes::serialize(UseBytesParams { - address: Address::new_id(client_id), - deal_size: BigInt::from(deal.proposal.piece_size.0), - })?, - TokenAmount::zero(), - ) { - info!("invalid deal {}: failed to acquire datacap exitcode: {}", di, e); - continue; + // For verified deals, transfer datacap tokens from the client + // to the verified registry actor along with a specification for the allocation. + // Drop deal if the transfer fails. + // This could be done in a batch, but one-at-a-time allows dropping of only + // some deals if the client's balance is insufficient, rather than dropping them all. + // An alternative could first fetch the available balance/allowance, and then make + // a batch transfer for an amount known to be available. + // https://github.com/filecoin-project/builtin-actors/issues/662 + let allocation_id = if deal.proposal.verified_deal { + let params = datacap_transfer_request( + &Address::new_id(client_id), + vec![alloc_request_for_deal(&deal, rt.policy(), curr_epoch)], + )?; + let alloc_ids = rt + .send( + &DATACAP_TOKEN_ACTOR_ADDR, + ext::datacap::TRANSFER_FROM_METHOD as u64, + serialize(¶ms, "transfer parameters")?, + TokenAmount::zero(), + ) + .and_then(|ret| datacap_transfer_response(&ret)); + match alloc_ids { + Ok(ids) => { + if ids.len() != 1 { + return Err(actor_error!( + unspecified, + "expected 1 allocation ID, got {:?}", + ids + )); + } + ids[0] + } + Err(e) => { + info!( + "invalid deal {}: failed to allocate datacap for verified deal: {}", + di, e + ); + continue; + } } - } + } else { + NO_ALLOCATION_ID + }; total_provider_lockup = provider_lockup; total_client_lockup.insert(client_id, client_lockup); proposal_cid_lookup.insert(pcid); - valid_proposal_cids.push(pcid); - valid_deals.push(deal); + valid_deals.push(ValidDeal { + proposal: deal.proposal, + cid: pcid, + allocation: allocation_id, + }); valid_input_bf.set(di as u64) } let valid_deal_count = valid_input_bf.len(); - if valid_deals.len() != valid_proposal_cids.len() { - return Err(actor_error!( - illegal_state, - "{} valid deals but {} valid proposal cids", - valid_deals.len(), - valid_proposal_cids.len() - )); - } if valid_deal_count != valid_deals.len() as u64 { return Err(actor_error!( illegal_state, @@ -430,38 +438,51 @@ impl Actor { })?; // 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 (vid, valid_deal) in valid_deals.iter().enumerate() { + for valid_deal in valid_deals.iter() { msm.lock_client_and_provider_balances(&valid_deal.proposal)?; - let id = msm.generate_storage_deal_id(); - - let pcid = valid_proposal_cids[vid]; - - msm.pending_deals.as_mut().unwrap().put(pcid.to_bytes().into()).map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to set pending deal") - })?; - msm.deal_proposals.as_mut().unwrap().set(id, valid_deal.proposal.clone()).map_err( - |e| e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to set deal"), - )?; + // 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")?; + // 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")?; + // 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", + )?; + } - // We randomize the first epoch for when the deal will be processed so an attacker isn't able to + // 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, id); + gen_rand_next_epoch(rt.policy(), valid_deal.proposal.start_epoch, deal_id); - msm.deals_by_epoch.as_mut().unwrap().put(process_epoch, id).map_err(|e| { + 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", ) })?; - new_deal_ids.push(id); + new_deal_ids.push(deal_id); } - msm.commit_state().map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to flush state") - })?; + msm.commit_state()?; Ok(()) })?; @@ -501,12 +522,7 @@ impl Actor { curr_epoch, Some(sector_size), ) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to validate deal proposals for activation", - ) - })?; + .context("failed to validate deal proposals for activation")?; let commd = if sector.deal_ids.is_empty() { None @@ -548,19 +564,14 @@ impl Actor { curr_epoch, None, ) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to validate deal proposals for activation", - ) - })? + .context("failed to validate deal proposals for activation")? }; // Update deal states rt.transaction(|st: &mut State, rt| { let mut msm = st.mutator(rt.store()); msm.with_deal_states(Permission::Write) - .with_pending_proposals(Permission::ReadOnly) + .with_pending_proposals(Permission::Write) .with_deal_proposals(Permission::ReadOnly) .build() .map_err(|e| { @@ -570,16 +581,18 @@ impl Actor { 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).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to get state for deal_id ({})", deal_id), - ) - })?; + 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) + })?; if s.is_some() { return Err(actor_error!( illegal_argument, - "deal {} already included in another sector", + "deal {} already activated", deal_id )); } @@ -589,22 +602,22 @@ impl Actor { .as_ref() .unwrap() .get(deal_id) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to get deal_id ({})", 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_id: {}", deal_id))?; + .ok_or_else(|| actor_error!(not_found, "no such deal proposal {}", deal_id))?; let propc = rt_deal_cid(rt, proposal)?; - let has = - msm.pending_deals.as_ref().unwrap().has(&propc.to_bytes()).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to get pending proposal ({})", propc), - ) + // 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) })?; if !has { @@ -615,6 +628,15 @@ 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) + })?; msm.deal_states .as_mut() .unwrap() @@ -624,20 +646,17 @@ impl Actor { sector_start_epoch: curr_epoch, last_updated_epoch: EPOCH_UNDEFINED, slash_epoch: EPOCH_UNDEFINED, + verified_claim: allocation + .unwrap_or((BytesKey(vec![]), NO_ALLOCATION_ID)) + .1, }, ) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to set deal state {}", deal_id), - ) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to set deal state {}", deal_id) })?; } - msm.commit_state().map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to flush state") - })?; - + msm.commit_state()?; Ok(()) })?; @@ -725,9 +744,7 @@ impl Actor { })?; } - msm.commit_state().map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to flush state") - })?; + msm.commit_state()?; Ok(()) })?; Ok(()) @@ -770,7 +787,6 @@ impl Actor { let mut amount_slashed = TokenAmount::zero(); let curr_epoch = rt.curr_epoch(); - let mut timed_out_verified_deals: Vec = Vec::new(); rt.transaction(|st: &mut State, rt| { let last_cron = st.last_cron; @@ -801,9 +817,7 @@ impl Actor { deal_ids.push(deal_id); Ok(()) }) - .map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to set deal state") - })?; + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to set deal state")?; for deal_id in deal_ids { let deal = msm @@ -811,11 +825,8 @@ impl Actor { .as_ref() .unwrap() .get(deal_id) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to get deal_id ({})", 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) @@ -829,12 +840,7 @@ impl Actor { .as_ref() .unwrap() .get(deal_id) - .map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - "failed to get deal state", - ) - })? + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to get deal state")? .cloned(); // deal has been published but not activated yet -> terminate it @@ -854,17 +860,15 @@ impl Actor { if !slashed.is_zero() { amount_slashed += slashed; } - if deal.verified_deal { - timed_out_verified_deals.push(deal); - } // Delete the proposal (but not state, which doesn't exist). - let deleted = - msm.deal_proposals.as_mut().unwrap().delete(deal_id).map_err(|e| { - e.downcast_default( - ExitCode::USR_ILLEGAL_STATE, - format!("failed to delete deal proposal {}", deal_id), - ) + 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) })?; if deleted.is_none() { return Err(actor_error!( @@ -875,15 +879,13 @@ impl Actor { ) )); } + // Delete pending deal CID 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 {}", deal_id), - ) + .with_context_code(ExitCode::USR_ILLEGAL_STATE, || { + format!("failed to delete pending proposal {}", deal_id) })? .ok_or_else(|| { actor_error!( @@ -891,6 +893,17 @@ impl Actor { "failed to delete pending proposal: 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 + ) + })?; continue; } @@ -1020,35 +1033,10 @@ impl Actor { msm.st.last_cron = rt.curr_epoch(); - msm.commit_state().map_err(|e| { - e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to flush state") - })?; + msm.commit_state()?; Ok(()) })?; - for d in timed_out_verified_deals { - let res = rt.send( - &VERIFIED_REGISTRY_ACTOR_ADDR, - ext::verifreg::RESTORE_BYTES_METHOD, - RawBytes::serialize(ext::verifreg::RestoreBytesParams { - address: d.client, - deal_size: BigInt::from(d.piece_size.0), - })?, - TokenAmount::zero(), - ); - if let Err(e) = res { - log::error!( - "failed to send RestoreBytes call to the verifreg actor for timed \ - out verified deal, client: {}, deal_size: {}, provider: {}, got code: {:?}. {}", - d.client, - d.piece_size.0, - d.provider, - e.exit_code(), - e.msg() - ); - } - } - if !amount_slashed.is_zero() { rt.send(&BURNT_FUNDS_ACTOR_ADDR, METHOD_SEND, RawBytes::default(), amount_slashed)?; } @@ -1091,7 +1079,7 @@ pub fn validate_and_return_deal_space( sector_expiry: ChainEpoch, sector_activation: ChainEpoch, sector_size: Option, -) -> anyhow::Result +) -> Result where BS: Blockstore, { @@ -1104,15 +1092,15 @@ where illegal_argument, "deal id {} present multiple times", deal_id - ) - .into()); + )); } let proposal = proposals - .get(*deal_id)? + .get(*deal_id) + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to load deal")? .ok_or_else(|| actor_error!(not_found, "no such deal {}", deal_id))?; validate_deal_can_activate(proposal, miner_addr, sector_expiry, sector_activation) - .map_err(|e| e.wrap(&format!("cannot activate deal {}", deal_id)))?; + .with_context(|| format!("cannot activate deal {}", deal_id))?; if proposal.verified_deal { verified_deal_space += proposal.piece_size.0; @@ -1128,14 +1116,60 @@ where "deals too large to fit in sector {} > {}", total_deal_space, sector_size - ) - .into()); + )); } } Ok(DealSpaces { deal_space, verified_deal_space }) } +fn alloc_request_for_deal( + deal: &ClientDealProposal, + policy: &Policy, + curr_epoch: ChainEpoch, +) -> ext::verifreg::AllocationRequest { + let alloc_term_min = deal.proposal.end_epoch - deal.proposal.start_epoch; + let alloc_term_max = min( + alloc_term_min + policy.market_default_allocation_term_buffer, + policy.maximum_verified_allocation_term, + ); + let alloc_expiration = + min(deal.proposal.start_epoch, curr_epoch + policy.maximum_verified_allocation_expiration); + ext::verifreg::AllocationRequest { + provider: deal.proposal.provider, + data: deal.proposal.piece_cid, + size: deal.proposal.piece_size, + term_min: alloc_term_min, + term_max: alloc_term_max, + expiration: alloc_expiration, + } +} + +// Builds TransferFromParams for a transfer of datacap for specified allocations. +fn datacap_transfer_request( + client: &Address, + alloc_reqs: Vec, +) -> Result { + let datacap_required = alloc_reqs.iter().map(|it| it.size.0 as i64).sum(); + Ok(TransferFromParams { + from: *client, + to: *VERIFIED_REGISTRY_ACTOR_ADDR, + amount: TokenAmount::from_whole(datacap_required), + operator_data: serialize( + &ext::verifreg::AllocationsRequest { requests: alloc_reqs }, + "allocation requests", + )?, + }) +} + +// Parses allocation IDs from a TransferFromReturn +fn datacap_transfer_response(ret: &RawBytes) -> Result, ActorError> { + let ret: TransferFromReturn = deserialize(ret, "transfer from response")?; + let allocs: ext::verifreg::AllocationsResponse = + deserialize(&ret.recipient_data, "allocations response")?; + Ok(allocs.allocations) +} + pub fn gen_rand_next_epoch( policy: &Policy, start_epoch: ChainEpoch, @@ -1318,6 +1352,25 @@ pub(crate) fn deal_cid(proposal: &DealProposal) -> Result { Ok(Cid::new_v1(DAG_CBOR, hash)) } +fn request_miner_control_addrs( + rt: &mut RT, + miner_id: ActorID, +) -> Result<(Address, Address, Vec
), ActorError> +where + BS: Blockstore, + RT: Runtime, +{ + let ret = rt.send( + &Address::new_id(miner_id), + ext::miner::CONTROL_ADDRESSES_METHOD, + RawBytes::default(), + TokenAmount::zero(), + )?; + let addrs: ext::miner::GetControlAddressesReturnParams = ret.deserialize()?; + + Ok((addrs.owner, addrs.worker, addrs.control_addresses)) +} + /// Resolves a provider or client address to the canonical form against which a balance should be held, and /// the designated recipient address of withdrawals (which is the same, for simple account parties). fn escrow_address( @@ -1383,6 +1436,11 @@ where Ok((ret.raw_byte_power, ret.quality_adj_power)) } +pub fn deal_id_key(k: DealID) -> BytesKey { + let bz = k.encode_var_vec(); + bz.into() +} + impl ActorCode for Actor { fn invoke_method( rt: &mut RT, diff --git a/actors/market/src/state.rs b/actors/market/src/state.rs index 9f0fe5873..4e62baede 100644 --- a/actors/market/src/state.rs +++ b/actors/market/src/state.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0, MIT 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, ActorDowncast, ActorError, Array, Set, SetMultimap, + actor_error, make_empty_map, make_map_with_root_and_bitwidth, ActorDowncast, ActorError, Array, + AsActorError, Map, Set, SetMultimap, }; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::tuple::*; @@ -62,6 +64,9 @@ pub struct State { pub total_provider_locked_collateral: TokenAmount, /// Total storage fee that is locked in escrow -> unlocked when payments are made pub total_client_storage_fee: TokenAmount, + + /// Verified registry allocation IDs for deals that are not yet activated. + pub pending_deal_allocation_ids: Cid, // HAMT[DealID]AllocationID } impl State { @@ -80,10 +85,13 @@ impl State { 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))?; + 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) + })?; Ok(Self { proposals: empty_proposals_array, states: empty_states_array, @@ -97,6 +105,7 @@ impl State { total_client_locked_collateral: TokenAmount::default(), total_provider_locked_collateral: TokenAmount::default(), total_client_storage_fee: TokenAmount::default(), + pending_deal_allocation_ids: empty_pending_deal_allocation_map, }) } @@ -172,6 +181,7 @@ pub(super) struct MarketStateMutation<'bs, 's, BS> { pub(super) pending_permit: Permission, pub(super) pending_deals: Option>, + pub(super) pending_deal_allocation_ids: Option>, pub(super) dpe_permit: Permission, pub(super) deals_by_epoch: Option>, @@ -202,6 +212,7 @@ where 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, @@ -236,6 +247,11 @@ where 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, + )?); } if self.dpe_permit != Permission::Invalid { @@ -278,25 +294,28 @@ where self } - pub(super) fn commit_state(&mut self) -> anyhow::Result<()> { + 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().map_err(|e| e.downcast_wrap("failed to flush deal proposals"))?; + self.st.proposals = s + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal proposals")?; } } if self.state_permit == Permission::Write { if let Some(s) = &mut self.deal_states { - self.st.states = - s.flush().map_err(|e| e.downcast_wrap("failed to flush deal states"))?; + self.st.states = s + .flush() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush deal states")?; } } if self.locked_permit == Permission::Write { if let Some(s) = &mut self.locked_table { - self.st.locked_table = - s.root().map_err(|e| e.downcast_wrap("failed to flush 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(); @@ -311,22 +330,31 @@ where if self.escrow_permit == Permission::Write { if let Some(s) = &mut self.escrow_table { - self.st.escrow_table = - s.root().map_err(|e| e.downcast_wrap("failed to flush escrow table"))?; + self.st.escrow_table = s + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; } } if self.pending_permit == Permission::Write { if let Some(s) = &mut self.pending_deals { - self.st.pending_proposals = - s.root().map_err(|e| e.downcast_wrap("failed to flush escrow table"))?; + 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", + )?; } } if self.dpe_permit == Permission::Write { if let Some(s) = &mut self.deals_by_epoch { - self.st.deal_ops_by_epoch = - s.root().map_err(|e| e.downcast_wrap("failed to flush escrow table"))?; + self.st.deal_ops_by_epoch = s + .root() + .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to flush escrow table")?; } } diff --git a/actors/market/src/testing.rs b/actors/market/src/testing.rs index e154cff2a..ff5699185 100644 --- a/actors/market/src/testing.rs +++ b/actors/market/src/testing.rs @@ -5,6 +5,7 @@ use std::{ }; use cid::Cid; +use fil_actors_runtime::builtin::HAMT_BIT_WIDTH; use fil_actors_runtime::{ make_map_with_root_and_bitwidth, parse_uint_key, MessageAccumulator, SetMultimap, }; @@ -15,8 +16,10 @@ use fvm_shared::{ deal::DealID, econ::TokenAmount, }; +use integer_encoding::VarInt; use num_traits::Zero; +use crate::ext::verifreg::AllocationID; use crate::{ balance_table::BalanceTable, deal_cid, DealArray, DealMetaArray, State, PROPOSALS_AMT_BITWIDTH, }; @@ -138,6 +141,26 @@ pub fn check_state_invariants( ), ); + let mut pending_allocations = BTreeMap::::new(); + match make_map_with_root_and_bitwidth(&state.pending_deal_allocation_ids, store, HAMT_BIT_WIDTH) + { + Ok(pending_allocations_hamt) => { + let ret = pending_allocations_hamt.for_each(|key, allocation_id| { + let deal_id: u64 = u64::decode_var(key.0.as_slice()).unwrap().0; + + acc.require( + proposal_stats.get(&deal_id).is_some(), + format!("pending deal allocation {} not found in proposals", deal_id), + ); + + pending_allocations.insert(deal_id, *allocation_id); + Ok(()) + }); + acc.require_no_error(ret, "error iterating pending allocations"); + } + Err(e) => acc.add(format!("error loading pending allocations: {e}")), + }; + // deal states let mut deal_state_count = 0; match DealMetaArray::load(&state.states, store) { @@ -172,6 +195,7 @@ pub fn check_state_invariants( } else { acc.add(format!("no deal proposal for deal state {deal_id}")); } + acc.require(!pending_allocations.contains_key(&deal_id), format!("deal {deal_id} has pending allocation")); deal_state_count += 1; @@ -184,7 +208,6 @@ pub fn check_state_invariants( // pending proposals let mut pending_proposal_count = 0; - match make_map_with_root_and_bitwidth::<_, ()>( &state.pending_proposals, store, diff --git a/actors/market/tests/cron_tick_timedout_deals.rs b/actors/market/tests/cron_tick_timedout_deals.rs index 4bc10d77d..5302333e9 100644 --- a/actors/market/tests/cron_tick_timedout_deals.rs +++ b/actors/market/tests/cron_tick_timedout_deals.rs @@ -1,19 +1,17 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use fil_actor_market::ext::verifreg::RestoreBytesParams; use fil_actor_market::{ - ext, Actor as MarketActor, ClientDealProposal, Method, PublishStorageDealsParams, + Actor as MarketActor, ClientDealProposal, Method, PublishStorageDealsParams, }; use fil_actors_runtime::network::EPOCHS_IN_DAY; use fil_actors_runtime::test_utils::*; -use fil_actors_runtime::{BURNT_FUNDS_ACTOR_ADDR, VERIFIED_REGISTRY_ACTOR_ADDR}; +use fil_actors_runtime::BURNT_FUNDS_ACTOR_ADDR; use fvm_ipld_encoding::RawBytes; use fvm_shared::clock::ChainEpoch; use fvm_shared::crypto::signature::Signature; use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; -use fvm_shared::sector::StoragePower; use fvm_shared::METHOD_SEND; use fil_actor_market::ext::account::{AuthenticateMessageParams, AUTHENTICATE_MESSAGE_METHOD}; @@ -138,7 +136,7 @@ fn publishing_timed_out_deal_again_should_work_after_cron_tick_as_it_should_no_l } #[test] -fn timed_out_and_verified_deals_are_slashed_deleted_and_sent_to_the_registry_actor() { +fn timed_out_and_verified_deals_are_slashed_deleted() { let mut rt = setup(); let mut deal1 = generate_deal_and_add_funds( &mut rt, @@ -172,39 +170,13 @@ fn timed_out_and_verified_deals_are_slashed_deleted_and_sent_to_the_registry_act &mut rt, &MinerAddresses::default(), &[deal1.clone(), deal2.clone(), deal3.clone()], + 1, ); // do a cron tick for it -> all should time out and get slashed // ONLY deal1 and deal2 should be sent to the Registry actor rt.set_epoch(process_epoch(START_EPOCH, *deal_ids.last().unwrap())); - // expected sends to the registry actor - let param1 = RestoreBytesParams { - address: deal1.client, - deal_size: StoragePower::from(deal1.piece_size.0), - }; - let param2 = RestoreBytesParams { - address: deal2.client, - deal_size: StoragePower::from(deal2.piece_size.0), - }; - - rt.expect_send( - *VERIFIED_REGISTRY_ACTOR_ADDR, - ext::verifreg::RESTORE_BYTES_METHOD as u64, - RawBytes::serialize(param1).unwrap(), - TokenAmount::zero(), - RawBytes::default(), - ExitCode::OK, - ); - rt.expect_send( - *VERIFIED_REGISTRY_ACTOR_ADDR, - ext::verifreg::RESTORE_BYTES_METHOD as u64, - RawBytes::serialize(param2).unwrap(), - TokenAmount::zero(), - RawBytes::default(), - ExitCode::OK, - ); - let expected_burn = 3 * &deal1.provider_collateral; rt.expect_send( *BURNT_FUNDS_ACTOR_ADDR, diff --git a/actors/market/tests/harness.rs b/actors/market/tests/harness.rs index b62a1816c..4d0e87e5d 100644 --- a/actors/market/tests/harness.rs +++ b/actors/market/tests/harness.rs @@ -1,29 +1,34 @@ #![allow(dead_code)] use cid::Cid; +use fil_fungible_token::token::types::{TransferFromParams, TransferFromReturn}; use num_traits::{FromPrimitive, Zero}; use regex::Regex; +use std::cmp::min; use std::{cell::RefCell, collections::HashMap}; use fil_actor_market::ext::account::{AuthenticateMessageParams, AUTHENTICATE_MESSAGE_METHOD}; +use fil_actor_market::ext::verifreg::{AllocationID, AllocationRequest, AllocationsResponse}; use fil_actor_market::{ - balance_table::BalanceTable, ext, ext::miner::GetControlAddressesReturnParams, + balance_table::BalanceTable, deal_id_key, ext, ext::miner::GetControlAddressesReturnParams, gen_rand_next_epoch, testing::check_state_invariants, ActivateDealsParams, ActivateDealsResult, Actor as MarketActor, ClientDealProposal, DealArray, DealMetaArray, DealProposal, DealState, Label, Method, OnMinerSectorsTerminateParams, PublishStorageDealsParams, PublishStorageDealsReturn, SectorDeals, State, VerifyDealsForActivationParams, - VerifyDealsForActivationReturn, WithdrawBalanceParams, WithdrawBalanceReturn, + VerifyDealsForActivationReturn, WithdrawBalanceParams, WithdrawBalanceReturn, NO_ALLOCATION_ID, PROPOSALS_AMT_BITWIDTH, }; use fil_actor_power::{CurrentTotalPowerReturn, Method as PowerMethod}; use fil_actor_reward::Method as RewardMethod; -use fil_actor_verifreg::UseBytesParams; +use fil_actors_runtime::builtin::HAMT_BIT_WIDTH; +use fil_actors_runtime::cbor::serialize; use fil_actors_runtime::{ + make_map_with_root_and_bitwidth, network::EPOCHS_IN_DAY, runtime::{Policy, Runtime}, test_utils::*, - ActorError, SetMultimap, BURNT_FUNDS_ACTOR_ADDR, CRON_ACTOR_ADDR, REWARD_ACTOR_ADDR, - STORAGE_MARKET_ACTOR_ADDR, STORAGE_POWER_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, + ActorError, SetMultimap, BURNT_FUNDS_ACTOR_ADDR, CRON_ACTOR_ADDR, DATACAP_TOKEN_ACTOR_ADDR, + REWARD_ACTOR_ADDR, STORAGE_MARKET_ACTOR_ADDR, STORAGE_POWER_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, VERIFIED_REGISTRY_ACTOR_ADDR, }; use fvm_ipld_encoding::{to_vec, RawBytes}; @@ -296,33 +301,35 @@ pub fn activate_deals_raw( pub fn get_deal_proposal(rt: &mut MockRuntime, deal_id: DealID) -> DealProposal { let st: State = rt.get_state(); - let deals = DealArray::load(&st.proposals, &rt.store).unwrap(); - let d = deals.get(deal_id).unwrap(); d.unwrap().clone() } -pub fn get_locked_balance(rt: &mut MockRuntime, addr: Address) -> TokenAmount { +pub fn get_pending_deal_allocation(rt: &mut MockRuntime, deal_id: DealID) -> AllocationID { let st: State = rt.get_state(); + let pending_allocations = + make_map_with_root_and_bitwidth(&st.pending_deal_allocation_ids, &rt.store, HAMT_BIT_WIDTH) + .unwrap(); - let lt = BalanceTable::from_root(&rt.store, &st.locked_table).unwrap(); + *pending_allocations.get(&deal_id_key(deal_id)).unwrap().unwrap_or(&NO_ALLOCATION_ID) +} +pub fn get_locked_balance(rt: &mut MockRuntime, addr: Address) -> TokenAmount { + let st: State = rt.get_state(); + let lt = BalanceTable::from_root(&rt.store, &st.locked_table).unwrap(); lt.get(&addr).unwrap() } pub fn get_deal_state(rt: &mut MockRuntime, deal_id: DealID) -> DealState { let st: State = rt.get_state(); - let states = DealMetaArray::load(&st.states, &rt.store).unwrap(); - let s = states.get(deal_id).unwrap(); *s.unwrap() } pub fn update_last_updated(rt: &mut MockRuntime, deal_id: DealID, new_last_updated: ChainEpoch) { let st: State = rt.get_state(); - let mut states = DealMetaArray::load(&st.states, &rt.store).unwrap(); let s = *states.get(deal_id).unwrap().unwrap(); @@ -333,7 +340,6 @@ pub fn update_last_updated(rt: &mut MockRuntime, deal_id: DealID, new_last_updat pub fn delete_deal_proposal(rt: &mut MockRuntime, deal_id: DealID) { let mut st: State = rt.get_state(); - let mut deals = DealArray::load(&st.proposals, &rt.store).unwrap(); deals.delete(deal_id).unwrap(); @@ -436,6 +442,7 @@ pub fn publish_deals( rt: &mut MockRuntime, addrs: &MinerAddresses, publish_deals: &[DealProposal], + next_allocation_id: AllocationID, ) -> Vec { rt.expect_validate_caller_type((*CALLER_TYPES_SIGNABLE).clone()); @@ -457,6 +464,7 @@ pub fn publish_deals( let mut params: PublishStorageDealsParams = PublishStorageDealsParams { deals: vec![] }; + let mut alloc_id = next_allocation_id; for deal in publish_deals { // create a client proposal with a valid signature let buf = RawBytes::serialize(deal.clone()).expect("failed to marshal deal proposal"); @@ -481,20 +489,44 @@ pub fn publish_deals( ); if deal.verified_deal { - let param = RawBytes::serialize(UseBytesParams { - address: deal.client, - deal_size: BigInt::from(deal.piece_size.0), - }) - .unwrap(); - + // Expect transfer of data cap to the verified registry, with spec for the allocation. + let curr_epoch = rt.epoch; + let alloc_req = ext::verifreg::AllocationsRequest { + requests: vec![AllocationRequest { + provider: deal.provider, + data: deal.piece_cid, + size: deal.piece_size, + term_min: deal.end_epoch - deal.start_epoch, + term_max: (deal.end_epoch - deal.start_epoch) + 90 * EPOCHS_IN_DAY, + expiration: min(deal.start_epoch, curr_epoch + 60 * EPOCHS_IN_DAY), + }], + }; + let datacap_amount = TokenAmount::from_whole(deal.piece_size.0 as i64); + let params = TransferFromParams { + from: deal.client, + to: *VERIFIED_REGISTRY_ACTOR_ADDR, + amount: datacap_amount.clone(), + operator_data: serialize(&alloc_req, "allocation requests").unwrap(), + }; + let alloc_ids = AllocationsResponse { allocations: vec![alloc_id] }; rt.expect_send( - *VERIFIED_REGISTRY_ACTOR_ADDR, - ext::verifreg::USE_BYTES_METHOD as u64, - param, + *DATACAP_TOKEN_ACTOR_ADDR, + ext::datacap::TRANSFER_FROM_METHOD as u64, + serialize(¶ms, "transfer from params").unwrap(), TokenAmount::zero(), - RawBytes::default(), + serialize( + &TransferFromReturn { + from_balance: TokenAmount::zero(), + to_balance: datacap_amount, + allowance: TokenAmount::zero(), + recipient_data: serialize(&alloc_ids, "allocation response").unwrap(), + }, + "transfer from return", + ) + .unwrap(), ExitCode::OK, ); + alloc_id += 1 } } @@ -511,11 +543,15 @@ pub fn publish_deals( assert_eq!(ret.ids.len(), publish_deals.len()); // assert state after publishing the deals + alloc_id = next_allocation_id; for (i, deal_id) in ret.ids.iter().enumerate() { let expected = &publish_deals[i]; let p = get_deal_proposal(rt, *deal_id); - assert_eq!(expected, &p); + if p.verified_deal { + assert_eq!(get_pending_deal_allocation(rt, *deal_id), alloc_id); + alloc_id += 1; + } } ret.ids @@ -768,7 +804,7 @@ pub fn publish_and_activate_deal( ) -> DealID { let deal = generate_deal_and_add_funds(rt, client, addrs, start_epoch, end_epoch); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, addrs.worker); - let deal_ids = publish_deals(rt, addrs, &[deal]); + let deal_ids = publish_deals(rt, addrs, &[deal], NO_ALLOCATION_ID); // unverified deal activate_deals(rt, sector_expiry, addrs.provider, current_epoch, &deal_ids); deal_ids[0] } @@ -782,7 +818,7 @@ pub fn generate_and_publish_deal( ) -> DealID { let deal = generate_deal_and_add_funds(rt, client, addrs, start_epoch, end_epoch); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, addrs.worker); - let deal_ids = publish_deals(rt, addrs, &[deal]); + let deal_ids = publish_deals(rt, addrs, &[deal], NO_ALLOCATION_ID); // unverified deal deal_ids[0] } @@ -792,11 +828,12 @@ pub fn generate_and_publish_verified_deal( addrs: &MinerAddresses, start_epoch: ChainEpoch, end_epoch: ChainEpoch, + next_allocation_id: AllocationID, ) -> DealID { let mut deal = generate_deal_and_add_funds(rt, client, addrs, start_epoch, end_epoch); deal.verified_deal = true; rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, addrs.worker); - let deal_ids = publish_deals(rt, addrs, &[deal]); + let deal_ids = publish_deals(rt, addrs, &[deal], next_allocation_id); deal_ids[0] } @@ -834,7 +871,7 @@ pub fn generate_and_publish_deal_for_piece( // publish rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, addrs.worker); - let deal_ids = publish_deals(rt, addrs, &[deal]); + let deal_ids = publish_deals(rt, addrs, &[deal], NO_ALLOCATION_ID); // unverified deal deal_ids[0] } diff --git a/actors/market/tests/market_actor_test.rs b/actors/market/tests/market_actor_test.rs index 72ad15a51..8a7da19e5 100644 --- a/actors/market/tests/market_actor_test.rs +++ b/actors/market/tests/market_actor_test.rs @@ -4,23 +4,23 @@ use fil_actor_market::balance_table::BALANCE_TABLE_BITWIDTH; use fil_actor_market::policy::detail::DEAL_MAX_LABEL_SIZE; use fil_actor_market::{ - ext, ActivateDealsParams, Actor as MarketActor, ClientDealProposal, DealMetaArray, Label, - Method, PublishStorageDealsParams, PublishStorageDealsReturn, State, WithdrawBalanceParams, - PROPOSALS_AMT_BITWIDTH, STATES_AMT_BITWIDTH, + deal_id_key, ext, ActivateDealsParams, Actor as MarketActor, ClientDealProposal, DealArray, + DealMetaArray, Label, Method, PublishStorageDealsParams, PublishStorageDealsReturn, State, + WithdrawBalanceParams, NO_ALLOCATION_ID, PROPOSALS_AMT_BITWIDTH, STATES_AMT_BITWIDTH, }; -use fil_actor_verifreg::UseBytesParams; -use fil_actors_runtime::cbor::deserialize; +use fil_actors_runtime::cbor::{deserialize, serialize}; use fil_actors_runtime::network::EPOCHS_IN_DAY; use fil_actors_runtime::runtime::{Policy, Runtime}; use fil_actors_runtime::test_utils::*; use fil_actors_runtime::{ - make_empty_map, ActorError, SetMultimap, BURNT_FUNDS_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, + make_empty_map, make_map_with_root_and_bitwidth, ActorError, Map, SetMultimap, + BURNT_FUNDS_ACTOR_ADDR, DATACAP_TOKEN_ACTOR_ADDR, SYSTEM_ACTOR_ADDR, VERIFIED_REGISTRY_ACTOR_ADDR, }; +use fil_fungible_token::token::types::{TransferFromParams, TransferFromReturn}; use fvm_ipld_amt::Amt; use fvm_ipld_encoding::{to_vec, RawBytes}; use fvm_shared::address::Address; -use fvm_shared::bigint::BigInt; use fvm_shared::clock::{ChainEpoch, EPOCH_UNDEFINED}; use fvm_shared::crypto::signature::Signature; use fvm_shared::deal::DealID; @@ -33,6 +33,7 @@ use regex::Regex; use std::ops::Add; use fil_actor_market::ext::account::{AuthenticateMessageParams, AUTHENTICATE_MESSAGE_METHOD}; +use fil_actor_market::ext::verifreg::{AllocationID, AllocationRequest, AllocationsResponse}; use num_traits::{FromPrimitive, Zero}; mod harness; @@ -684,28 +685,95 @@ fn simple_deal() { let mut rt = setup(); rt.set_epoch(publish_epoch); + let next_allocation_id = 1; // Publish from miner worker. - let deal1 = generate_deal_and_add_funds( + let mut deal1 = generate_deal_and_add_funds( &mut rt, CLIENT_ADDR, &MinerAddresses::default(), start_epoch, end_epoch, ); + deal1.verified_deal = false; rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &MinerAddresses::default(), &[deal1]); + let deal1_id = + publish_deals(&mut rt, &MinerAddresses::default(), &[deal1], next_allocation_id)[0]; // Publish from miner control address. - let deal2 = generate_deal_and_add_funds( + let mut deal2 = generate_deal_and_add_funds( &mut rt, CLIENT_ADDR, &MinerAddresses::default(), start_epoch + 1, end_epoch + 1, ); + deal2.verified_deal = true; rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, CONTROL_ADDR); - publish_deals(&mut rt, &MinerAddresses::default(), &[deal2]); + let deal2_id = + publish_deals(&mut rt, &MinerAddresses::default(), &[deal2], next_allocation_id)[0]; + + // activate the deal + activate_deals(&mut rt, end_epoch + 1, PROVIDER_ADDR, publish_epoch, &[deal1_id, deal2_id]); + let deal1st = get_deal_state(&mut rt, deal1_id); + assert_eq!(publish_epoch, deal1st.sector_start_epoch); + assert_eq!(NO_ALLOCATION_ID, deal1st.verified_claim); + + let deal2st = get_deal_state(&mut rt, deal2_id); + assert_eq!(publish_epoch, deal2st.sector_start_epoch); + assert_eq!(next_allocation_id, deal2st.verified_claim); + + check_state(&rt); +} + +#[test] +fn deal_expires() { + let start_epoch = 100; + let end_epoch = start_epoch + 200 * EPOCHS_IN_DAY; + let publish_epoch = ChainEpoch::from(1); + + let mut rt = setup(); + rt.set_epoch(publish_epoch); + let next_allocation_id = 1; + + // Publish from miner worker. + let mut deal = generate_deal_and_add_funds( + &mut rt, + CLIENT_ADDR, + &MinerAddresses::default(), + start_epoch, + end_epoch, + ); + deal.verified_deal = true; + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); + let deal_id = + publish_deals(&mut rt, &MinerAddresses::default(), &[deal.clone()], next_allocation_id)[0]; + + rt.set_epoch(start_epoch + EPOCHS_IN_DAY + 1); + rt.expect_send( + *BURNT_FUNDS_ACTOR_ADDR, + METHOD_SEND, + RawBytes::default(), + deal.provider_collateral, + RawBytes::default(), + ExitCode::OK, + ); + cron_tick(&mut rt); + + // No deal state for unactivated deal + let st: State = rt.get_state(); + let states = DealMetaArray::load(&st.states, &rt.store).unwrap(); + assert!(states.get(deal_id).unwrap().is_none()); + + // The proposal is gone + assert!(DealArray::load(&st.proposals, &rt.store).unwrap().get(deal_id).unwrap().is_none()); + + // Pending allocation ID is gone + let pending_allocs: Map<_, AllocationID> = + make_map_with_root_and_bitwidth(&st.pending_deal_allocation_ids, &rt.store, HAMT_BIT_WIDTH) + .unwrap(); + assert!(pending_allocs.get(&deal_id_key(deal_id)).unwrap().is_none()); + check_state(&rt); } @@ -794,19 +862,40 @@ fn provider_and_client_addresses_are_resolved_before_persisting_state_and_sent_t ExitCode::OK, ); - // request is sent to the VerifReg actor using the resolved address - let param = RawBytes::serialize(UseBytesParams { - address: client_resolved, - deal_size: BigInt::from(deal.piece_size.0), - }) - .unwrap(); - + // Data cap transfer is requested using the resolved address (not that it matters). + let alloc_req = ext::verifreg::AllocationsRequest { + requests: vec![AllocationRequest { + provider: provider_resolved, + data: deal.piece_cid, + size: deal.piece_size, + term_min: deal.end_epoch - deal.start_epoch, + term_max: (deal.end_epoch - deal.start_epoch) + 90 * EPOCHS_IN_DAY, + expiration: deal.start_epoch, + }], + }; + let datacap_amount = TokenAmount::from_whole(deal.piece_size.0 as i64); + let transfer_params = TransferFromParams { + from: client_resolved, + to: *VERIFIED_REGISTRY_ACTOR_ADDR, + amount: datacap_amount.clone(), + operator_data: serialize(&alloc_req, "allocation requests").unwrap(), + }; + let transfer_return = TransferFromReturn { + from_balance: TokenAmount::zero(), + to_balance: datacap_amount, + allowance: TokenAmount::zero(), + recipient_data: serialize( + &AllocationsResponse { allocations: vec![1] }, + "allocations response", + ) + .unwrap(), + }; rt.expect_send( - *VERIFIED_REGISTRY_ACTOR_ADDR, - ext::verifreg::USE_BYTES_METHOD as u64, - param, + *DATACAP_TOKEN_ACTOR_ADDR, + ext::datacap::TRANSFER_FROM_METHOD as u64, + serialize(&transfer_params, "transfer from params").unwrap(), TokenAmount::zero(), - RawBytes::default(), + serialize(&transfer_return, "transfer from return").unwrap(), ExitCode::OK, ); @@ -898,7 +987,7 @@ fn publish_a_deal_with_enough_collateral_when_circulating_supply_is_superior_to_ // publish the deal successfully rt.set_epoch(publish_epoch); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &MinerAddresses::default(), &[deal]); + publish_deals(&mut rt, &MinerAddresses::default(), &[deal], 1); check_state(&rt); } @@ -945,6 +1034,7 @@ fn publish_multiple_deals_for_different_clients_and_ensure_balances_are_correct( &mut rt, &MinerAddresses::default(), &[deal1.clone(), deal2.clone(), deal3.clone()], + 1, ); // assert locked balance for all clients and provider @@ -984,7 +1074,7 @@ fn publish_multiple_deals_for_different_clients_and_ensure_balances_are_correct( 100 + 200 * EPOCHS_IN_DAY, ); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &MinerAddresses::default(), &[deal4.clone(), deal5.clone()]); + publish_deals(&mut rt, &MinerAddresses::default(), &[deal4.clone(), deal5.clone()], 1); // assert locked balances for clients and provider let provider_locked_expected = @@ -1027,7 +1117,7 @@ fn publish_multiple_deals_for_different_clients_and_ensure_balances_are_correct( // publish both the deals for the second provider rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &addrs, &[deal6.clone(), deal7.clone()]); + publish_deals(&mut rt, &addrs, &[deal6.clone(), deal7.clone()], 1); // assertions let st: State = rt.get_state(); @@ -1578,7 +1668,7 @@ fn market_actor_deals() { // First attempt at publishing the deal should work rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &miner_addresses, &[deal_proposal.clone()]); + publish_deals(&mut rt, &miner_addresses, &[deal_proposal.clone()], 1); // Second attempt at publishing the same deal should fail publish_deals_expect_abort( @@ -1591,7 +1681,7 @@ fn market_actor_deals() { // Same deal with a different label should work deal_proposal.label = Label::String("Cthulhu".to_owned()); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &miner_addresses, &[deal_proposal]); + publish_deals(&mut rt, &miner_addresses, &[deal_proposal], 1); check_state(&rt); } @@ -1617,7 +1707,7 @@ fn max_deal_label_size() { // DealLabel at max size should work. deal_proposal.label = Label::String("s".repeat(DEAL_MAX_LABEL_SIZE)); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - publish_deals(&mut rt, &miner_addresses, &[deal_proposal.clone()]); + publish_deals(&mut rt, &miner_addresses, &[deal_proposal.clone()], 1); // over max should fail deal_proposal.label = Label::String("s".repeat(DEAL_MAX_LABEL_SIZE + 1)); diff --git a/actors/market/tests/on_miner_sectors_terminate.rs b/actors/market/tests/on_miner_sectors_terminate.rs index a150711ea..0e06137ab 100644 --- a/actors/market/tests/on_miner_sectors_terminate.rs +++ b/actors/market/tests/on_miner_sectors_terminate.rs @@ -162,7 +162,7 @@ fn terminate_valid_deals_along_with_expired_and_cleaned_up_deal() { ); rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - let deal_ids = publish_deals(&mut rt, &MinerAddresses::default(), &[deal1, deal2.clone()]); + let deal_ids = publish_deals(&mut rt, &MinerAddresses::default(), &[deal1, deal2.clone()], 1); activate_deals(&mut rt, sector_expiry, PROVIDER_ADDR, current_epoch, &deal_ids); let new_epoch = end_epoch - 1; diff --git a/actors/market/tests/verify_deals_for_activation_test.rs b/actors/market/tests/verify_deals_for_activation_test.rs index 3a8c1d346..281cfa722 100644 --- a/actors/market/tests/verify_deals_for_activation_test.rs +++ b/actors/market/tests/verify_deals_for_activation_test.rs @@ -65,6 +65,7 @@ fn verify_deal_and_activate_to_get_deal_space_for_verified_deal_proposal() { &MINER_ADDRESSES, START_EPOCH, END_EPOCH, + 1, ); let deal_proposal = get_deal_proposal(&mut rt, deal_id); @@ -116,7 +117,7 @@ fn verification_and_weights_for_verified_and_unverified_deals() { ]; rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, WORKER_ADDR); - let deal_ids = publish_deals(&mut rt, &MINER_ADDRESSES, &deals.clone()); + let deal_ids = publish_deals(&mut rt, &MINER_ADDRESSES, &deals.clone(), 1); let response = verify_deals_for_activation( &mut rt, diff --git a/actors/verifreg/src/lib.rs b/actors/verifreg/src/lib.rs index 8a8f3f647..82f7d5cc1 100644 --- a/actors/verifreg/src/lib.rs +++ b/actors/verifreg/src/lib.rs @@ -615,10 +615,14 @@ impl Actor { Ok(GetClaimsReturn { batch_info: batch_gen.gen(), claims }) } + // Receives data cap tokens (only) and creates allocations according to one or more + // allocation requests specified in the transfer's operator data. + // The token amount received must exactly correspond to the sum of the requested allocation sizes. + // Returns the ids of the created allocations. pub fn universal_receiver_hook( rt: &mut RT, params: UniversalReceiverParams, - ) -> Result<(), ActorError> + ) -> Result where BS: Blockstore, RT: Runtime, @@ -659,10 +663,10 @@ impl Actor { }) } // Save allocations - rt.transaction(|st: &mut State, rt| { + let ids = rt.transaction(|st: &mut State, rt| { st.insert_allocations(rt.store(), client, new_allocs.into_iter()) })?; - Ok(()) + Ok(AllocationsResponse { allocations: ids }) } } @@ -1072,8 +1076,8 @@ impl ActorCode for Actor { Ok(RawBytes::serialize(res)?) } Some(Method::UniversalReceiverHook) => { - Self::universal_receiver_hook(rt, cbor::deserialize_params(params)?)?; - Ok(RawBytes::default()) + let res = Self::universal_receiver_hook(rt, cbor::deserialize_params(params)?)?; + Ok(RawBytes::serialize(res)?) } Some(Method::GetClaims) => { let res = Self::get_claims(rt, cbor::deserialize_params(params)?)?; diff --git a/actors/verifreg/src/state.rs b/actors/verifreg/src/state.rs index 0c6696be5..0ace59345 100644 --- a/actors/verifreg/src/state.rs +++ b/actors/verifreg/src/state.rs @@ -150,7 +150,7 @@ impl State { store: &BS, client: ActorID, new_allocs: I, - ) -> Result<(), ActorError> + ) -> Result, ActorError> where I: Iterator, { @@ -172,7 +172,8 @@ impl State { .context_code(ExitCode::USR_ILLEGAL_STATE, "failed to put allocations")?; self.save_allocs(&mut allocs)?; self.next_allocation_id += count; - Ok(()) + let allocated_ids = (first_id..first_id + count).collect(); + Ok(allocated_ids) } pub fn load_claims<'a, BS: Blockstore>( diff --git a/actors/verifreg/src/types.rs b/actors/verifreg/src/types.rs index c4fd4cff2..5355e91dd 100644 --- a/actors/verifreg/src/types.rs +++ b/actors/verifreg/src/types.rs @@ -175,6 +175,14 @@ pub struct AllocationRequests { } impl Cbor for AllocationRequests {} +/// Recipient data payload in response to a datacap token transfer. +#[derive(Clone, Debug, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)] +pub struct AllocationsResponse { + pub allocations: Vec, +} + +impl Cbor for AllocationsResponse {} + #[derive(Debug, Serialize_tuple, Deserialize_tuple)] pub struct GetClaimsParams { diff --git a/actors/verifreg/tests/harness/mod.rs b/actors/verifreg/tests/harness/mod.rs index c2c88178a..4af45ffdf 100644 --- a/actors/verifreg/tests/harness/mod.rs +++ b/actors/verifreg/tests/harness/mod.rs @@ -18,10 +18,10 @@ use num_traits::{ToPrimitive, Zero}; use fil_actor_verifreg::testing::check_state_invariants; use fil_actor_verifreg::{ ext, Actor as VerifregActor, AddVerifierClientParams, AddVerifierParams, Allocation, - AllocationID, AllocationRequest, AllocationRequests, ClaimAllocationsParams, - ClaimAllocationsReturn, ClaimID, DataCap, GetClaimsParams, GetClaimsReturn, Method, - RemoveExpiredAllocationsParams, RemoveExpiredAllocationsReturn, RestoreBytesParams, - SectorAllocationClaim, State, + AllocationID, AllocationRequest, AllocationRequests, AllocationsResponse, + ClaimAllocationsParams, ClaimAllocationsReturn, ClaimID, DataCap, GetClaimsParams, + GetClaimsReturn, Method, RemoveExpiredAllocationsParams, RemoveExpiredAllocationsReturn, + RestoreBytesParams, SectorAllocationClaim, State, }; use fil_actors_runtime::cbor::serialize; use fil_actors_runtime::runtime::policy_constants::{ @@ -332,6 +332,7 @@ impl Harness { &self, rt: &mut MockRuntime, payload: FRC46TokenReceived, + expected_alloc_ids: Vec, ) -> Result<(), ActorError> { rt.set_caller(*DATACAP_TOKEN_ACTOR_CODE_ID, *DATACAP_TOKEN_ACTOR_ADDR); let params = UniversalReceiverParams { @@ -344,7 +345,10 @@ impl Harness { Method::UniversalReceiverHook as MethodNum, &serialize(¶ms, "hook params").unwrap(), )?; - assert_eq!(RawBytes::default(), ret); + assert_eq!( + RawBytes::serialize(AllocationsResponse { allocations: expected_alloc_ids }).unwrap(), + ret + ); rt.verify(); Ok(()) } diff --git a/actors/verifreg/tests/verifreg_actor_test.rs b/actors/verifreg/tests/verifreg_actor_test.rs index bb05158bd..f13afc695 100644 --- a/actors/verifreg/tests/verifreg_actor_test.rs +++ b/actors/verifreg/tests/verifreg_actor_test.rs @@ -587,7 +587,7 @@ mod datacap { make_alloc_req(&rt, PROVIDER2, ALLOC_SIZE * 2), ]; let payload = make_receiver_hook_token_payload(CLIENT1, reqs.clone()); - h.receive_tokens(&mut rt, payload).unwrap(); + h.receive_tokens(&mut rt, payload, vec![1, 2]).unwrap(); // Verify allocations in state. let st: State = rt.get_state(); @@ -608,7 +608,7 @@ mod datacap { // Make another allocation from a different client let reqs = vec![make_alloc_req(&rt, PROVIDER1, ALLOC_SIZE)]; let payload = make_receiver_hook_token_payload(CLIENT2, reqs.clone()); - h.receive_tokens(&mut rt, payload).unwrap(); + h.receive_tokens(&mut rt, payload, vec![3]).unwrap(); // Verify allocations in state. let st: State = rt.get_state(); @@ -695,7 +695,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, format!("allocation provider {} must be a miner actor", provider1).as_str(), - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![1]), ); h.check_state(&rt); } @@ -712,7 +712,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "allocation size 1048575 below minimum 1048576", - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![]), ); } // Min term too short @@ -723,7 +723,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "allocation term min 518399 below limit 518400", - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![]), ); } // Max term too long @@ -734,7 +734,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "allocation term max 5259486 above limit 5259485", - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![]), ); } // Term minimum greater than maximum @@ -746,7 +746,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "allocation term min 2103795 exceeds term max 2103794", - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![]), ); } // Allocation expires too late @@ -756,8 +756,8 @@ mod datacap { let payload = make_receiver_hook_token_payload(CLIENT1, reqs); expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, - "allocation expiration 86401 exceeds maximum 86400", - h.receive_tokens(&mut rt, payload), + "allocation expiration 172801 exceeds maximum 172800", + h.receive_tokens(&mut rt, payload, vec![]), ); } // Tokens received doesn't match sum of allocation sizes @@ -771,7 +771,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "total allocation size 2097152 must match data cap amount received 2097153", - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![]), ); } // One bad request fails the lot @@ -785,7 +785,7 @@ mod datacap { expect_abort_contains_message( ExitCode::USR_ILLEGAL_ARGUMENT, "allocation size 1048575 below minimum 1048576", - h.receive_tokens(&mut rt, payload), + h.receive_tokens(&mut rt, payload, vec![]), ); } h.check_state(&rt); diff --git a/runtime/src/runtime/policy.rs b/runtime/src/runtime/policy.rs index 12a01cd4e..ca86cbe23 100644 --- a/runtime/src/runtime/policy.rs +++ b/runtime/src/runtime/policy.rs @@ -159,6 +159,10 @@ pub struct Policy { /// supply that must be covered by provider collateral pub prov_collateral_percent_supply_denom: i64, + /// The default duration after a verified deal's nominal term to set for the corresponding + /// allocation's maximum term. + pub market_default_allocation_term_buffer: i64, + // --- power --- /// Minimum miner consensus power #[serde(with = "bigint_ser")] @@ -242,6 +246,8 @@ impl Default for Policy { policy_constants::PROV_COLLATERAL_PERCENT_SUPPLY_NUM, prov_collateral_percent_supply_denom: policy_constants::PROV_COLLATERAL_PERCENT_SUPPLY_DENOM, + market_default_allocation_term_buffer: + policy_constants::MARKET_DEFAULT_ALLOCATION_TERM_BUFFER, minimum_consensus_power: StoragePower::from(policy_constants::MINIMUM_CONSENSUS_POWER), } @@ -380,7 +386,7 @@ pub mod policy_constants { pub const MINIMUM_VERIFIED_ALLOCATION_SIZE: i32 = 256; pub const MINIMUM_VERIFIED_ALLOCATION_TERM: i64 = 180 * EPOCHS_IN_DAY; pub const MAXIMUM_VERIFIED_ALLOCATION_TERM: i64 = 5 * EPOCHS_IN_YEAR; - pub const MAXIMUM_VERIFIED_ALLOCATION_EXPIRATION: i64 = 30 * EPOCHS_IN_DAY; + pub const MAXIMUM_VERIFIED_ALLOCATION_EXPIRATION: i64 = 60 * EPOCHS_IN_DAY; /// DealUpdatesInterval is the number of blocks between payouts for deals pub const DEAL_UPDATES_INTERVAL: i64 = EPOCHS_IN_DAY; @@ -396,6 +402,8 @@ pub mod policy_constants { /// supply that must be covered by provider collateral pub const PROV_COLLATERAL_PERCENT_SUPPLY_DENOM: i64 = 100; + pub const MARKET_DEFAULT_ALLOCATION_TERM_BUFFER: i64 = 90 * EPOCHS_IN_DAY; + #[cfg(feature = "min-power-2k")] pub const MINIMUM_CONSENSUS_POWER: i64 = 2 << 10; #[cfg(feature = "min-power-2g")] diff --git a/test_vm/Cargo.toml b/test_vm/Cargo.toml index 9ccf0ecb9..f4732876d 100644 --- a/test_vm/Cargo.toml +++ b/test_vm/Cargo.toml @@ -25,26 +25,28 @@ fil_actor_market = { version = "9.0.0-alpha.1", path = "../actors/market" } fil_actor_verifreg = { version = "9.0.0-alpha.1", path = "../actors/verifreg" } fil_actor_miner = { version = "9.0.0-alpha.1", path = "../actors/miner" } fil_actor_datacap = { version = "9.0.0-alpha.1", path = "../actors/datacap" } -lazy_static = "1.4.0" -fvm_shared = { version = "2.0.0-alpha.2", default-features = false } -fvm_ipld_encoding = { version = "0.2.2", default-features = false } -fvm_ipld_blockstore = { version = "0.1.1", default-features = false } + +anyhow = "1.0.56" +bimap = { version = "0.6.2" } +blake2b_simd = "1.0" +cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } +fil_fungible_token = { git = "https://github.com/helix-onchain/filecoin", rev = "39320534b8e992b9c5e53e15641ea4c9c113ed3e" } fvm_ipld_bitfield = "0.5.2" +fvm_ipld_blockstore = { version = "0.1.1", default-features = false } +fvm_ipld_encoding = { version = "0.2.2", default-features = false } fvm_ipld_hamt = "0.5.1" -num-traits = "0.2.14" -num-derive = "0.3.3" +fvm_shared = { version = "2.0.0-alpha.2", default-features = false } +indexmap = { version = "1.8.0", features = ["serde-1"] } +integer-encoding = { version = "3.0.3", default-features = false } +lazy_static = "1.4.0" log = "0.4.14" +num-derive = "0.3.3" +num-traits = "0.2.14" rand = "0.8.5" rand_chacha = "0.3.1" -indexmap = { version = "1.8.0", features = ["serde-1"] } -cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } +regex = "1" serde = { version = "1.0.136", features = ["derive"] } thiserror = "1.0.30" -anyhow = "1.0.56" -bimap = { version = "0.6.2" } -blake2b_simd = "1.0" -integer-encoding = { version = "3.0.3", default-features = false } -regex = "1" [dev-dependencies] cid = { version = "0.8.3", default-features = false, features = ["serde-codec"] } diff --git a/test_vm/src/util.rs b/test_vm/src/util.rs index 21bf09024..db64142c7 100644 --- a/test_vm/src/util.rs +++ b/test_vm/src/util.rs @@ -7,6 +7,7 @@ use fil_actor_market::{ PublishStorageDealsReturn, }; +use fil_actor_market::ext::verifreg::{AllocationRequest, AllocationsRequest}; use fil_actor_miner::{ aggregate_pre_commit_network_fee, max_prove_commit_duration, new_deadline_info_from_offset_and_epoch, Deadline, DeadlineInfo, DeclareFaultsRecoveredParams, @@ -21,6 +22,13 @@ use fil_actor_power::{ }; use fil_actor_reward::Method as RewardMethod; use fil_actor_verifreg::{Method as VerifregMethod, VerifierParams}; +use fil_actors_runtime::runtime::policy_constants::{ + MARKET_DEFAULT_ALLOCATION_TERM_BUFFER, MAXIMUM_VERIFIED_ALLOCATION_EXPIRATION, +}; +use fil_fungible_token::receiver::types::{ + FRC46TokenReceived, UniversalReceiverParams, FRC46_TOKEN_TYPE, +}; +use fil_fungible_token::token::types::TransferFromParams; use fvm_ipld_bitfield::{BitField, UnvalidatedBitField}; use fvm_ipld_encoding::{BytesDe, Cbor, RawBytes}; use fvm_shared::address::{Address, BLS_PUB_LEN}; @@ -565,7 +573,7 @@ pub fn add_verifier(v: &VM, verifier: Address, data_cap: StoragePower) { #[allow(clippy::too_many_arguments)] pub fn publish_deal( v: &VM, - provider: Address, + worker: Address, deal_client: Address, miner_id: Address, deal_label: String, @@ -600,7 +608,7 @@ pub fn publish_deal( }; let ret: PublishStorageDealsReturn = apply_ok( v, - provider, + worker, *STORAGE_MARKET_ACTOR_ADDR, TokenAmount::zero(), MarketMethod::PublishStorageDeals as u64, @@ -632,9 +640,55 @@ pub fn publish_deal( }, ]; if verified_deal { + let deal_term = deal.end_epoch - deal.start_epoch; + let token_amount = TokenAmount::from_whole(deal.piece_size.0 as i64); + let alloc_expiration = + min(deal.start_epoch, v.curr_epoch + MAXIMUM_VERIFIED_ALLOCATION_EXPIRATION); + + let alloc_reqs = AllocationsRequest { + requests: vec![AllocationRequest { + provider: miner_id, + data: deal.piece_cid, + size: deal.piece_size, + term_min: deal_term, + term_max: deal_term + MARKET_DEFAULT_ALLOCATION_TERM_BUFFER, + expiration: alloc_expiration, + }], + }; expect_publish_invocs.push(ExpectInvocation { - to: *VERIFIED_REGISTRY_ACTOR_ADDR, - method: VerifregMethod::UseBytes as u64, + to: *DATACAP_TOKEN_ACTOR_ADDR, + method: DataCapMethod::TransferFrom as u64, + params: Some( + RawBytes::serialize(&TransferFromParams { + from: deal_client, + to: *VERIFIED_REGISTRY_ACTOR_ADDR, + amount: token_amount.clone(), + operator_data: RawBytes::serialize(&alloc_reqs).unwrap(), + }) + .unwrap(), + ), + code: Some(ExitCode::OK), + subinvocs: Some(vec![ExpectInvocation { + to: *VERIFIED_REGISTRY_ACTOR_ADDR, + method: VerifregMethod::UniversalReceiverHook as u64, + params: Some( + RawBytes::serialize(&UniversalReceiverParams { + type_: FRC46_TOKEN_TYPE, + payload: RawBytes::serialize(&FRC46TokenReceived { + from: deal_client.id().unwrap(), + to: VERIFIED_REGISTRY_ACTOR_ADDR.id().unwrap(), + operator: STORAGE_MARKET_ACTOR_ADDR.id().unwrap(), + amount: token_amount, + operator_data: RawBytes::serialize(&alloc_reqs).unwrap(), + token_data: Default::default(), + }) + .unwrap(), + }) + .unwrap(), + ), + code: Some(ExitCode::OK), + ..Default::default() + }]), ..Default::default() }) } diff --git a/test_vm/tests/terminate_test.rs b/test_vm/tests/terminate_test.rs index 36ddff82e..4c6612a71 100644 --- a/test_vm/tests/terminate_test.rs +++ b/test_vm/tests/terminate_test.rs @@ -46,7 +46,7 @@ fn terminate_sectors() { let sealed_cid = make_sealed_cid(b"s100"); let seal_proof = RegisteredSealProof::StackedDRG32GiBV1P1; - let (id_addr, robust_addr) = create_miner( + let (miner_id_addr, miner_robust_addr) = create_miner( &mut v, owner, worker, @@ -96,7 +96,7 @@ fn terminate_sectors() { *STORAGE_MARKET_ACTOR_ADDR, miner_collateral.clone(), MarketMethod::AddBalance as u64, - id_addr, + miner_id_addr, ); // create 3 deals, some verified and some not @@ -106,7 +106,7 @@ fn terminate_sectors() { &v, worker, verified_client, - id_addr, + miner_id_addr, "deal1".to_string(), PaddedPieceSize(1 << 30), true, @@ -120,7 +120,7 @@ fn terminate_sectors() { &v, worker, verified_client, - id_addr, + miner_id_addr, "deal2".to_string(), PaddedPieceSize(1 << 32), true, @@ -134,7 +134,7 @@ fn terminate_sectors() { &v, worker, unverified_client, - id_addr, + miner_id_addr, "deal3".to_string(), PaddedPieceSize(1 << 34), false, @@ -166,7 +166,7 @@ fn terminate_sectors() { apply_ok( &v, worker, - robust_addr, + miner_robust_addr, TokenAmount::zero(), MinerMethod::PreCommitSector as u64, PreCommitSectorParams { @@ -180,14 +180,14 @@ fn terminate_sectors() { }, ); let prove_time = v.get_epoch() + Policy::default().pre_commit_challenge_delay + 1; - let v = advance_by_deadline_to_epoch(v, id_addr, prove_time).0; + let v = advance_by_deadline_to_epoch(v, miner_id_addr, prove_time).0; // prove commit, cron, advance to post time let prove_params = ProveCommitSectorParams { sector_number, proof: vec![] }; apply_ok( &v, worker, - robust_addr, + miner_robust_addr, TokenAmount::zero(), MinerMethod::ProveCommitSector as u64, prove_params, @@ -202,12 +202,12 @@ fn terminate_sectors() { ) .unwrap(); assert_eq!(ExitCode::OK, res.code); - let (dline_info, p_idx, v) = advance_to_proving_deadline(v, id_addr, sector_number); + let (dline_info, p_idx, v) = advance_to_proving_deadline(v, miner_id_addr, sector_number); let d_idx = dline_info.index; - let st = v.get_state::(id_addr).unwrap(); + let st = v.get_state::(miner_id_addr).unwrap(); let sector = st.get_sector(v.store, sector_number).unwrap().unwrap(); let sector_power = power_for_sector(seal_proof.sector_size().unwrap(), §or); - submit_windowed_post(&v, worker, id_addr, dline_info, p_idx, Some(sector_power)); + submit_windowed_post(&v, worker, miner_id_addr, dline_info, p_idx, Some(sector_power)); let v = v.with_epoch(dline_info.last()); v.apply_message( @@ -225,7 +225,7 @@ fn terminate_sectors() { let v = v.with_epoch(start); // get out of proving deadline so we don't post twice let v = advance_by_deadline_to_epoch_while_proving( v, - id_addr, + miner_id_addr, worker, sector_number, start + Policy::default().deal_updates_interval, @@ -244,7 +244,7 @@ fn terminate_sectors() { apply_ok( &v, worker, - robust_addr, + miner_robust_addr, TokenAmount::zero(), MinerMethod::TerminateSectors as u64, TerminateSectorsParams { @@ -256,7 +256,7 @@ fn terminate_sectors() { }, ); ExpectInvocation { - to: id_addr, + to: miner_id_addr, method: MinerMethod::TerminateSectors as u64, subinvocs: Some(vec![ ExpectInvocation { @@ -294,7 +294,7 @@ fn terminate_sectors() { } .matches(v.take_invocations().last().unwrap()); - let miner_balances = v.get_miner_balance(id_addr); + let miner_balances = v.get_miner_balance(miner_id_addr); assert!(miner_balances.initial_pledge.is_zero()); assert!(miner_balances.pre_commit_deposit.is_zero()); @@ -319,7 +319,7 @@ fn terminate_sectors() { // advance a market cron processing period to process terminations fully let (v, _) = advance_by_deadline_to_epoch( v, - id_addr, + miner_id_addr, termination_epoch + Policy::default().deal_updates_interval, ); // because of rounding error it's annoying to compute exact withdrawable balance which is 2.9999.. FIL @@ -352,7 +352,7 @@ fn terminate_sectors() { *STORAGE_MARKET_ACTOR_ADDR, TokenAmount::zero(), MarketMethod::WithdrawBalance as u64, - WithdrawBalanceParams { provider_or_client: id_addr, amount: miner_collateral }, + WithdrawBalanceParams { provider_or_client: miner_id_addr, amount: miner_collateral }, ); let value_withdrawn = v.take_invocations().last().unwrap().subinvocations[1].msg.value();