Skip to content

Update then burn in verifreg's universal_receiver_hook #1219

Open
@Stebalien

Description

@Stebalien

This should be safe right now as burn calls a trusted actor, but it could become an issue if we're not careful. Ideally we'd:

  1. Update the state in a transaction.
  2. Burn, rolling back the state update on failure.

See:

pub fn universal_receiver_hook(
rt: &mut impl Runtime,
params: UniversalReceiverParams,
) -> Result<AllocationsResponse, ActorError> {
// Accept only the data cap token.
rt.validate_immediate_caller_is(&[DATACAP_TOKEN_ACTOR_ADDR])?;
let my_id = rt.message().receiver().id().unwrap();
let curr_epoch = rt.curr_epoch();
// Validate receiver hook payload.
let tokens_received = validate_tokens_received(&params, my_id)?;
let client = tokens_received.from;
// Extract and validate allocation request from the operator data.
let reqs: AllocationRequests =
deserialize(&tokens_received.operator_data, "allocation requests")?;
let mut datacap_total = DataCap::zero();
// Construct new allocation records.
let mut new_allocs = Vec::with_capacity(reqs.allocations.len());
for req in &reqs.allocations {
validate_new_allocation(req, rt.policy(), curr_epoch)?;
// Require the provider for new allocations to be a miner actor.
// This doesn't matter much, but is more ergonomic to fail rather than lock up datacap.
check_miner_id(rt, req.provider)?;
new_allocs.push(Allocation {
client,
provider: req.provider,
data: req.data,
size: req.size,
term_min: req.term_min,
term_max: req.term_max,
expiration: req.expiration,
});
datacap_total += DataCap::from(req.size.0);
}
let st: State = rt.state()?;
let mut claims = st.load_claims(rt.store())?;
let mut updated_claims = Vec::<(ClaimID, Claim)>::new();
let mut extension_total = DataCap::zero();
for req in &reqs.extensions {
// Note: we don't check the client address here, by design.
// Any client can spend datacap to extend an existing claim.
let claim = state::get_claim(&mut claims, req.provider, req.claim)?
.with_context_code(ExitCode::USR_NOT_FOUND, || {
format!("no claim {} for provider {}", req.claim, req.provider)
})?;
let policy = rt.policy();
validate_claim_extension(req, claim, policy, curr_epoch)?;
// The claim's client is not changed to be the address of the token sender.
// It remains the original allocation client.
updated_claims.push((req.claim, Claim { term_max: req.term_max, ..*claim }));
datacap_total += DataCap::from(claim.size.0);
extension_total += DataCap::from(claim.size.0);
}
// Allocation size must match the tokens received exactly (we don't return change).
let tokens_as_datacap = tokens_to_datacap(&tokens_received.amount);
if datacap_total != tokens_as_datacap {
return Err(actor_error!(
illegal_argument,
"total allocation size {} must match data cap amount received {}",
datacap_total,
tokens_as_datacap
));
}
// Burn the received datacap tokens spent on extending existing claims.
// The tokens spent on new allocations will be burnt when claimed later, or refunded.
burn(rt, &extension_total)?;
// Partial success isn't supported yet, but these results make space for it in the future.
let allocation_results = BatchReturn::ok(new_allocs.len() as u32);
let extension_results = BatchReturn::ok(updated_claims.len() as u32);
// Save new allocations and updated claims.
let ids = rt.transaction(|st: &mut State, rt| {
let ids = st.insert_allocations(rt.store(), client, new_allocs)?;
st.put_claims(rt.store(), updated_claims)?;
Ok(ids)
})?;
Ok(AllocationsResponse { allocation_results, extension_results, new_allocations: ids })
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions