-
Notifications
You must be signed in to change notification settings - Fork 755
feat: host function promise_set_refund_to
#14285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Adding the host function `promise_refund_to` is part of the deterministic account id proposal. This requires a new field somewhere on the receipt. I think putting it on the ActionReceipt makes the most sense. Adding a new version of the `ActionReceipt` is a bit cumbersome. In this commit, I'm trying to do just the minimal required change to get things working, to keep the PR size from exploding. - Most places that don't need to set the new field keep using the old version. - The old version is not renamed. (`ActionReceipt` rather than `ActionReceiptV1`) These two points can be cleaned up in future refactor-only PRs.
this is necessary to fix some tests
On the latest master HEAD, the new host function needs to be defined twice, to work with wasmtime, too.
|
notes for reviewers:
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #14285 +/- ##
==========================================
- Coverage 69.61% 69.59% -0.03%
==========================================
Files 982 982
Lines 208883 209209 +326
Branches 208883 209209 +326
==========================================
+ Hits 145413 145595 +182
- Misses 57219 57346 +127
- Partials 6251 6268 +17
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
50a9dd2 to
1a26b0a
Compare
1a26b0a to
abf19cb
Compare
core/store/src/utils/mod.rs
Outdated
| }; | ||
| set(state_update, key, receipt); | ||
| } | ||
| ReceiptEnum::PromiseYieldV2(action_receipt) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are quite a few places where such code duplication happens as part of match statement which would be nice to avoid. Maybe lets add fn from_receipt(receipt: &Receipt) -> Self as part VersionedActionReceipt which asserts the right receipt type internally. Then we can used binding as part of the match statement: receipt @ ReceiptEnum::PromiseYield(_) | ReceiptEnum::PromiseYieldV2(_) => { action_receipt = VersionedActionReceipt::from_receipt(receipt); .... That this is not ideal because we replace compile-time check with runtime assert on receipt type, but might be worth it to avoid code duplication.
I think a better idea would be to introduce transient VersionedReceiptEnum data structure similar to VersionedActionReceipt. It will have Action and PromiseYield variants containing VersionedActionReceipt and other variants are the same as in ReceiptEnum. Then we can eliminate code duplication while keeping type safety. The downside of that approach is that it is more involved to implement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we can used binding as part of the match statement: receipt @ ReceiptEnum::PromiseYield() | ReceiptEnum::PromiseYieldV2() => { action_receipt = VersionedActionReceipt::from_receipt(receipt); .... That this is not ideal because we replace compile-time check with runtime assert on receipt type, but might be worth it to avoid code duplication.
Personally, I don't hate code duplication (of "simple" code) enough to see this as a benefit. :) But I can see your argument.
I think a better idea would be to introduce transient
VersionedReceiptEnumdata structure similar toVersionedActionReceipt. It will haveActionandPromiseYieldvariants containingVersionedActionReceiptand other variants are the same as inReceiptEnum. Then we can eliminate code duplication while keeping type safety. The downside of that approach is that it is more involved to implement.
This sounds good! Implementation shouldn't be too bad. I would be more worried about code readability with all the indirections. But I think it's worth it. Will give it a try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented this in the latest commit
fb524f3
It allowed me to remove some annoying code duplication. E.g. how we process receipt in runtime/runtime/src/lib.rs
@@ -1149,7 +1146,7 @@ impl Runtime {
}
}
}
- ReceiptEnum::Action(action_receipt) => {
+ VersionedReceiptEnum::Action(action_receipt) => {
// Received a new action receipt. We'll first check how many input data items
// were already received before and saved in the state.
// And if we have all input data, then we can immediately execute the receipt.
@@ -1164,41 +1161,19 @@ impl Runtime {
pipeline_manager,
stats,
account_id,
- action_receipt.into(),
- )?;
-
- if executed.is_some() {
- return Ok(executed);
- }
- }
- ReceiptEnum::ActionV2(action_receipt) => {
- // Received a new action receipt. We'll first check how many input data items
- // were already received before and saved in the state.
- // And if we have all input data, then we can immediately execute the receipt.
- // If not, then we will postpone this receipt for later.
- let executed = self.process_action_receipt(
- receipt,
- receipt_sink,
- validator_proposals,
- state_update,
- apply_state,
- epoch_info_provider,
- pipeline_manager,
- stats,
- account_id,
- action_receipt.into(),
+ action_receipt,
)?;I couldn't make it work for mutable access, which still needs to explicitly match all versions. So code like this still has duplication.
// runtime/runtime/src/lib.rs
// Modifying a new receipt instead of sending data
match result
.new_receipts
.get_mut(receipt_index as usize)
.expect("the receipt for the given receipt index should exist")
.receipt_mut()
{
ReceiptEnum::Action(new_action_receipt)
| ReceiptEnum::PromiseYield(new_action_receipt) => new_action_receipt
.output_data_receivers
.extend_from_slice(&action_receipt.output_data_receivers()),
ReceiptEnum::ActionV2(new_action_receipt)
| ReceiptEnum::PromiseYieldV2(new_action_receipt) => new_action_receipt
.output_data_receivers
.extend_from_slice(&action_receipt.output_data_receivers()),
_ => unreachable!("the receipt should be an action receipt"),
}Also, the runtime sync calls tests with assert_receipts! still needs to list all versions, too, as we need to extract actions from different versions.
// runtime/runtime/tests/test_async_calls.rs
let receipts = &*assert_receipts!(group, "near_1" => r1 @ "near_2",
ReceiptEnum::Action(ActionReceipt{actions, ..}) | ReceiptEnum::ActionV2(ActionReceiptV2{actions, ..}),
{},
actions,
a0, Action::FunctionCall(function_call_action), {
assert_eq!(function_call_action.gas, GAS_2);
assert_eq!(function_call_action.deposit, 0);
});Overall, I think it's probably worth the extra boiler-plate and indirection, to avoid code duplication in a few key places. It probably also makes future changes easier. (Though it could also make it harder, I'm bad at predicting these things)
@pugachAG Is this what you had in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is exactly what I meant, thanks!
Regarding mutable access in my understanding the only way around it is introducing dedicated wrappers VersionedReceiptEnumMut and VersionedReceiptEnumMut which probably isn't worth it at this point.
As suggested in the review process, this avoids code duplication by adding another versioned convenience wrapper, this time around ReceiptEnum.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest keeping only the test_async_calls test as it is on the lower level (testing smaller unit) which is generally preferred
runtime/runtime/src/lib.rs
Outdated
| // Received a new action receipt. We'll first check how many input data items | ||
| // were already received before and saved in the state. | ||
| // And if we have all input data, then we can immediately execute the receipt. | ||
| // If not, then we will postpone this receipt for later. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it seems like this comment belongs as part of the extracted method
| if self.block_accounts.contains(account_id) { | ||
| return false; | ||
| } | ||
| let actions = match receipt.receipt() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: would be good to used versioned receipt here as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing a trick but the Rust borrow checker doesn't allow borrowing VersionedReceiptEnum::Action(a) => a.actions() beyond the lifetime of a as things are written now. So I'd have to refactor the code to make it work.
removing the test loop test, keeping the async calls test
New struct: ActionReceiptV2 with hash 182527260 This is used in two new variants added to the end of `ReceiptEnum`. Many structs through out the protocol contain this enum. 54 hashes need to be updated accordingly.
|
@frol Sorry GitHub pulled you in as reviewer because an intermediate merge used an older |
With
promise_set_refund_to, an outgoing action receipt can set where the balance refund should go, in case of a balance refund. Balance refunds happen after a failed receipt with attached deposits.Adding the host function is part of of NEP-616. (near/NEPs#616).
It helps with cases where the signing account calls a deterministic account, which then calls another deterministic account. If the second call needs a balance attached, the signing account may have provided it for forwarding. In that case, if the second calls fails, we want the funds to go back to the signer, not the caller.
The implementation is involved. It requires a new field somewhere on the receipt. I think putting it on the ActionReceipt makes the most sense. This is the first time we modify
ActionReceiptin NEAR history, as far as I know.I propose to make ActionReceipt versioned, by adding more variants to
ReceiptEnum. The variant implicitly defines the version. Note that this needs to be done forReceiptEnum::ActionandReceiptEnum::PromiseYieldsince both contain an action receipt.To make changes around the code base easier, I then added
pub enum VersionedActionReceipt<'a>which can be constructed from eitherActionReceiptorActionReceiptV2and has access methods to all fields.I also considered making
ActionReceiptitself versioned instead of expandingReceiptEnum. But for backwards compatibility with borsh encoding, I believe it would still need new variants inReceiptEnum.We could only go to
ReceiptEnum::VersionedActioninstead ofReceiptEnum::ActionV2. But I don't see how it would be better than my proposed solution.Testing strategy
Tests check that the new host function can be called from a contract and correctly redirects the refund, if the feature is enabled.