Skip to content
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

[aptos-framework] Dispatchable Token Standard #12635

Merged
merged 1 commit into from
Apr 25, 2024

Conversation

runtian-zhou
Copy link
Contributor

@runtian-zhou runtian-zhou commented Mar 22, 2024

Description

This PR provides a reference implementation for AIP-73. See discussion thread: #12490.

The PR mainly have the following four parts:

  • MoveVM related change to:
    • support native functions to return control flow change directive.
    • Add runtime check for cyclic call graph that could be introduced by the dispatch logic
  • A function_info.move that mocks the function pointer
  • A overloadable_fungible_asset.move that utilizes the function info and dispatch native to allow for a custom withdraw operation
  • An example of how a overloadable withdraw function could be implemented as a test.
  • WIP: Compiler directive to check the dispatch target function has expected type so the users can get early warnings on type mismatch?

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Performance improvement
  • Refactoring
  • Dependency update
  • Documentation update

Which Components or Systems Does This Change Impact?

  • Validator Node
  • Full Node (API, Indexer, etc.)
  • Move/Aptos Virtual Machine
  • Aptos Framework
  • Aptos CLI/SDK
  • Developer Infrastructure
  • Other (specify)

How Has This Been Tested?

Added tests for all logics added.

Key Areas to Review

A few areas we should focus on:

  1. The runtime check implemented in MoveVM is indeed safe.
  2. Make sure we charge gas for dispatch properly.
  3. Work with ecosystem folks on the public interfaces of dispatchable token module.

Checklist

  • I have read and followed the CONTRIBUTING doc
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I identified and added all stakeholders and component owners affected by this change as reviewers
  • I tested both happy and unhappy path of the functionality
  • I have made corresponding changes to the documentation

Copy link

trunk-io bot commented Mar 22, 2024

⏱️ 2h 47m total CI duration on this PR
Job Cumulative Duration Recent Runs
execution-performance / single-node-performance 36m 🟥
run-tests-main-branch 36m 🟥🟥🟥🟥🟥 (+1 more)
rust-unit-tests 26m 🟥
windows-build 23m 🟩
rust-move-unit-coverage 18m 🟩
rust-targeted-unit-tests 6m
rust-lints 5m 🟥
rust-move-tests 4m 🟥
check 4m 🟩
general-lints 2m 🟩
check-dynamic-deps 2m 🟩
file_change_determinator 1m 🟩🟩🟩🟩🟩 (+1 more)
semgrep/ci 51s 🟩🟩
permission-check 24s 🟩🟩🟩🟩🟩 (+1 more)
permission-check 19s 🟩🟩🟩🟩🟩 (+1 more)
permission-check 18s 🟩🟩🟩🟩🟩 (+1 more)
file_change_determinator 16s 🟩
permission-check 14s 🟩🟩🟩🟩🟩 (+1 more)

🚨 1 job on the last run was significantly faster/slower than expected

Job Duration vs 7d avg Delta
run-tests-main-branch 6m 4m +51%

settingsfeedbackdocs ⋅ learn more about trunk.io

@runtian-zhou
Copy link
Contributor Author

I tried to separate out the components into each individual commits. So reviewing by commit might be better.

Copy link

codecov bot commented Mar 22, 2024

Codecov Report

Attention: Patch coverage is 69.62233% with 185 lines in your changes are missing coverage. Please review.

Project coverage is 62.4%. Comparing base (3b3edf1) to head (984aa7e).
Report is 2 commits behind head on main.

Files Patch % Lines
aptos-move/aptos-vm/src/aptos_vm.rs 0.0% 94 Missing ⚠️
aptos-move/aptos-vm/src/transaction_validation.rs 0.0% 34 Missing ⚠️
...hird_party/move/move-vm/runtime/src/interpreter.rs 89.9% 20 Missing ⚠️
...ptos-vm/src/verifier/transaction_arg_validation.rs 0.0% 8 Missing ⚠️
aptos-move/framework/src/natives/function_info.rs 93.2% 8 Missing ⚠️
...s-gas-schedule/src/gas_schedule/aptos_framework.rs 0.0% 7 Missing ⚠️
aptos-move/aptos-vm/src/testing.rs 0.0% 4 Missing ⚠️
aptos-move/aptos-vm/src/validator_txns/dkg.rs 0.0% 2 Missing ⚠️
aptos-move/aptos-vm/src/validator_txns/jwk.rs 0.0% 2 Missing ⚠️
...party/move/move-vm/runtime/src/native_functions.rs 92.3% 2 Missing ⚠️
... and 3 more
Additional details and impacted files
@@            Coverage Diff            @@
##             main   #12635     +/-   ##
=========================================
+ Coverage    62.3%    62.4%   +0.1%     
=========================================
  Files         828      830      +2     
  Lines      185844   186363    +519     
=========================================
+ Hits       115858   116390    +532     
+ Misses      69986    69973     -13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@alnoki alnoki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way that this support can be extended for primary fungible stores?

Asking since those will be the primary use case for FA transfers

Comment on lines 127 to 139
/// A transfer with a fixed amount debited from the sender
public fun transfer_fixed_send<T: key>(
_sender: &signer,
from: Object<T>,
to: Object<T>,
send_amount: u64,
) acquires OverloadFunctionStore {
let store_address = object::object_address(&from);
assert!(exists<OverloadFunctionStore>(store_address), error::not_found(EFUNCTION_STORE_NOT_FOUND));
let overloadable_store = borrow_global<OverloadFunctionStore>(store_address);
let fa = fungible_asset::withdraw_with_ref(&overloadable_store.transfer_ref, from, send_amount);
deposit(to, fa);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

So in this case the deposit function is the one that might implement a tax?

Asking because I don't see an assert for EAMOUNT_MISMATCH

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the assumption here is we use withdraw_with_ref to get the exact amount from the sender side. So we don't need EAMOUNT_MISMATCH

let fa = withdraw(sender, from, receive_amount);
let store_address = object::object_address(&from);
let overloadable_store = borrow_global<OverloadFunctionStore>(store_address);
assert!(fungible_asset::amount(&fa) == receive_amount, error::aborted(EAMOUNT_MISMATCH));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here, the assumption is that once a hot potato FA has been withdrawn, the exact amount will necessarily end up in the receiver store

Correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes because we use deposit_with_ref so it has no custom behavior.

Comment on lines 91 to 125
public fun withdraw<T: key>(
owner: &signer,
store: Object<T>,
amount: u64,
): FungibleAsset acquires OverloadFunctionStore {
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store));
let owner_address = signer::address_of(owner);
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND));
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr);
dispatchable_withdraw(
owner_address,
store,
amount,
&overloadable_store.transfer_ref,
&overloadable_store.withdraw_function,
)
}

/// Deposit `amount` of the fungible asset to `store`.
///
/// The semantics of deposit will be governed by the function specified in OverloadFunctionStore.
public fun deposit<T: key>(
store: Object<T>,
fa: FungibleAsset
) acquires OverloadFunctionStore {
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store));
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND));
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr);
dispatchable_deposit(
store,
fa,
&overloadable_store.transfer_ref,
&overloadable_store.deposit_function,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens for the two-sided transfer functions in the case that

a. withdraw assesses a tax
b. deposit assesses a tax
c. both assess a tax

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an interesting case. To avoid double assess maybe we should just use the transfer api so at most one side pays the tax?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the issue? I already pay two taxes in the US ... income and sales 😭

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidiw my main concern here is ensuring that a transfer can have either a known amount sent or received

else the accounting for DeFi breaks, since you don't have a known amount deposited into a pool (known receive amount) or withdrawn from a pool (known send amount)

@vgao1996 vgao1996 self-requested a review March 25, 2024 17:24
@runtian-zhou runtian-zhou force-pushed the runtianz/dispatchable_token branch 2 times, most recently from ff16f49 to 8328a2c Compare March 25, 2024 19:26
fungible_asset::deposit_with_ref(&overloadable_store.transfer_ref, to, fa);
}

native fun dispatchable_withdraw<T: key>(
Copy link
Contributor

@junkil-park junkil-park Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we cannot specify this function because, at the static time, function is unknown, and we do not know the exact behavior of this native function dispatchable_withdraw.

It would be useful to observe some instances of overloaded functions and see what properties we can specify for them.


#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct OverloadFunctionStore has key {
withdraw_function: FunctionInfo,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe these should be optional and we need balance and something else, @movekevin

Comment on lines 91 to 95
public fun withdraw<T: key>(
owner: &signer,
store: Object<T>,
amount: u64,
): FungibleAsset acquires OverloadFunctionStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a wrapper around withdraw, the happy path is that the FA isn't frozen and that this isn't used. We'll also need to see performance of the global freeze.

Comment on lines 91 to 125
public fun withdraw<T: key>(
owner: &signer,
store: Object<T>,
amount: u64,
): FungibleAsset acquires OverloadFunctionStore {
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store));
let owner_address = signer::address_of(owner);
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND));
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr);
dispatchable_withdraw(
owner_address,
store,
amount,
&overloadable_store.transfer_ref,
&overloadable_store.withdraw_function,
)
}

/// Deposit `amount` of the fungible asset to `store`.
///
/// The semantics of deposit will be governed by the function specified in OverloadFunctionStore.
public fun deposit<T: key>(
store: Object<T>,
fa: FungibleAsset
) acquires OverloadFunctionStore {
let metadata_addr = object::object_address(&fungible_asset::store_metadata(store));
assert!(exists<OverloadFunctionStore>(metadata_addr), error::not_found(EFUNCTION_STORE_NOT_FOUND));
let overloadable_store = borrow_global<OverloadFunctionStore>(metadata_addr);
dispatchable_deposit(
store,
fa,
&overloadable_store.transfer_ref,
&overloadable_store.deposit_function,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the issue? I already pay two taxes in the US ... income and sales 😭

Comment on lines 133 to 134
) acquires OverloadFunctionStore {
let metadata_addr = object::object_address(&fungible_asset::store_metadata(from));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something seems off here where is the ownership check?

@runtian-zhou runtian-zhou force-pushed the runtianz/dispatchable_token branch 2 times, most recently from fda30ca to 637170e Compare March 28, 2024 00:55
@@ -100,6 +100,11 @@ module aptos_framework::fungible_asset {
project_uri: String,
}

#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct GlobalFreeze has key {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freeze has a different meaning.

Let's call this differently / more descriptively, and not share error codes.

maybe DelegatedToOverloaded? ActionsOverloaded?
also, can only one of the withdraw/deposit be overloaded, or do both need to be simultanously? or if either is, we just ask them to go through the other api anyways?

Copy link
Contributor

@zekun000 zekun000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall looks reasonable to me

@@ -139,9 +142,14 @@ impl Interpreter {
.map_err(|e| self.set_location(e))?;
}

if let Some(module_id) = function.module_id() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess it's impossible to re-enter a script?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea script has to be always on the top most frame only.

mut arguments: VecDeque<Value>,
) -> SafeNativeResult<SmallVec<[Value; 1]>> {
let (module_name, func_name) = extract_function_info(&mut arguments)?;
Err(SafeNativeError::CallFunction {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems pretty hacky

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed a hack, but it's due to a limitation of our current implementation -- native functions lack access to the real gas meter, so to do the metering properly, we exit from the native function and then instruct the Move VM to do the actual work.

We have considered various alternatives and non of them is trivial.

string::utf8(b"dispatchable_withdraw"),
);
// Verify that caller type matches callee type so wrongly typed function cannot be registered.
assert!(function_info::check_dispatch_type_compatibility(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we ensure the function is public? otherwise it can be upgraded and even change the function signature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to be even stricter, and require it to be un-upgradeable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it needs to be a public function -- otherwise this check needs to be performed each transfer.

I think it ought to be upgradeable -- not sure why we'd restrict it that way.

string::utf8(b"dispatchable_withdraw"),
);
// Verify that caller type matches callee type so wrongly typed function cannot be registered.
assert!(function_info::check_dispatch_type_compatibility(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to be even stricter, and require it to be un-upgradeable?

@runtian-zhou runtian-zhou force-pushed the runtianz/dispatchable_token branch 8 times, most recently from 22c88ae to dd6450c Compare April 8, 2024 22:04

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@@ -45,3 +45,13 @@ proposals:
secrecy_threshold_in_percentage: 50
reconstruct_threshold_in_percentage: 66
fast_path_secrecy_threshold_in_percentage: 67
- name: step_5_dispatchable_token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't add features in the default release yaml file anymore, they will be enabled separately. if you want you can create a dispatchable.yaml file in the same folder

@runtian-zhou runtian-zhou force-pushed the runtianz/dispatchable_token branch 2 times, most recently from f5bec0c to 417e662 Compare April 25, 2024 18:02
@igor-aptos igor-aptos removed the CICD:run-execution-performance-full-test Run execution performance test (full version) label Apr 25, 2024
@runtian-zhou runtian-zhou force-pushed the runtianz/dispatchable_token branch 2 times, most recently from 7d9b67b to 2e0f00c Compare April 25, 2024 18:17

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

✅ Forge suite realistic_env_max_load success on 984aa7efffb7fe269d458888ba4b9d1843c03e7c

two traffics test: inner traffic : committed: 7732 txn/s, latency: 5070 ms, (p50: 4800 ms, p90: 6000 ms, p99: 11100 ms), latency samples: 3340340
two traffics test : committed: 100 txn/s, latency: 2041 ms, (p50: 1800 ms, p90: 2200 ms, p99: 8200 ms), latency samples: 1760
Latency breakdown for phase 0: ["QsBatchToPos: max: 0.205, avg: 0.201", "QsPosToProposal: max: 0.281, avg: 0.248", "ConsensusProposalToOrdered: max: 0.472, avg: 0.436", "ConsensusOrderedToCommit: max: 0.406, avg: 0.383", "ConsensusProposalToCommit: max: 0.858, avg: 0.819"]
Max round gap was 1 [limit 4] at version 1467624. Max no progress secs was 6.476503 [limit 15] at version 1467624.
Test Ok

Copy link
Contributor

✅ Forge suite compat success on aptos-node-v1.10.1 ==> 984aa7efffb7fe269d458888ba4b9d1843c03e7c

Compatibility test results for aptos-node-v1.10.1 ==> 984aa7efffb7fe269d458888ba4b9d1843c03e7c (PR)
1. Check liveness of validators at old version: aptos-node-v1.10.1
compatibility::simple-validator-upgrade::liveness-check : committed: 5327 txn/s, latency: 5682 ms, (p50: 5000 ms, p90: 9900 ms, p99: 11700 ms), latency samples: 229100
2. Upgrading first Validator to new version: 984aa7efffb7fe269d458888ba4b9d1843c03e7c
compatibility::simple-validator-upgrade::single-validator-upgrade : committed: 1836 txn/s, latency: 15675 ms, (p50: 18300 ms, p90: 22300 ms, p99: 22600 ms), latency samples: 91840
3. Upgrading rest of first batch to new version: 984aa7efffb7fe269d458888ba4b9d1843c03e7c
compatibility::simple-validator-upgrade::half-validator-upgrade : committed: 1846 txn/s, latency: 15672 ms, (p50: 19000 ms, p90: 22600 ms, p99: 23100 ms), latency samples: 90460
4. upgrading second batch to new version: 984aa7efffb7fe269d458888ba4b9d1843c03e7c
compatibility::simple-validator-upgrade::rest-validator-upgrade : committed: 3481 txn/s, latency: 9219 ms, (p50: 9600 ms, p90: 12700 ms, p99: 13000 ms), latency samples: 146240
5. check swarm health
Compatibility test for aptos-node-v1.10.1 ==> 984aa7efffb7fe269d458888ba4b9d1843c03e7c passed
Test Ok

@runtian-zhou runtian-zhou merged commit bbf569a into main Apr 25, 2024
53 checks passed
@runtian-zhou runtian-zhou deleted the runtianz/dispatchable_token branch April 25, 2024 20:12
@@ -263,20 +267,25 @@ impl SharedTestingConfig {
VMResult<Vec<Vec<u8>>>,
TestRunInfo,
) {
let move_vm = MoveVM::new(self.native_function_table.clone()).unwrap();
let mut config = VMConfig::default();
config.paranoid_type_checks = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runtian-zhou added a commit that referenced this pull request May 3, 2024
sherry-x pushed a commit that referenced this pull request May 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CICD:run-execution-performance-test Run execution performance test
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet