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

Resolve harmony-one/bounties#77: Staking precompiles #3906

Merged
merged 38 commits into from
Jan 21, 2022

Conversation

MaxMustermann2
Copy link
Contributor

@MaxMustermann2 MaxMustermann2 commented Oct 22, 2021

This PR adds staking precompiled contracts to the Harmony EVM, at address 0xFC after a hard fork at EpochTBD. The precompiles can be called from Solidity by using abi.encodeWithSelector on the required parameters to produce the arguments for assembly call. This will allow any contract to participate in staking. 3/5 directives (Delegate, Undelegate and CollectRewards are accessible from the precompiled contract, which follows exactly the same logic as the originalApplyStakingMessage.

A sample Solidity implementation is outlined here. The repository also contains integration tests for the (modified) node; the test report is available below. Note that a contract may only use the precompile for its own address; otherwise, the node will return an error indicating address mismatch. One instance of such a test case is covered in the sample implementation as test_delegate_fail_malicious. Additionally, I have added a test case to ensure that the precompile can be called multiple times in one transaction (test_multiple_calls_success). This can be used, for example, by a staking contract, to delegate 50% of its ONE to one validator, 25% to another, and 25% to a third validator - all in a single transaction hash.

Commit message:

Create write capable precompiles that can perform staking transactions
Add hard fork logic (EpochTBD) for these precompiles
Tests for new code with at least 80% unit test coverage
Staking library + tests in MaxMustermann2/harmony-staking-precompiles

Issue

harmony-one/bounties#77 | Gitcoin

Bounty Requirements

1.1 Core protocol

  • Add staking logic to precompiles.
    • Precompiles can be directly called with staking data as arguments.
    • The execution of the precompiles will return result and error as necessary.
    • The execution logic will be exactly the same as ApplyStakingTransaction.
    • Staking directive:
      • Create Validator
      • Edit Validator
      • Delegate
      • Undelegate
      • Collect Rewards
  • Add hard fork logic for the new precompiles.
    • Aggregate the new precompile contracts to a new set.
    • Add the fork logic in evm run and Call.
    • Add the fork epoch number to localnet at LocalnetChainConfig.
    • Misc
      • New added code shall have at least 80% coverage unit test (see Link)
      • Propose PRs to merge code.
      • Test reports running localnet with detailed test steps and results (see MaxMustermann2/harmony-staking-precompiles)
      • Test fork logic so that there is no compatibility issue.

1.2 Smart contract

  • A smart contract served as a staking library (see MaxMustermann2/harmony-staking-precompiles)
    • Contains five pure methods which is a one to one mapping to staking directive.
    • Smart contract deployed to testnet and mainnet after the fork.
    • Tests and examples
      • Unit test included in the repository.
      • Add example contracts to call the staking library.
      • The integration test that include some corner cases of staking.
      • Test cases can be referenced from harmony integration test.

1.3 Test reports

  • All test reports shall contain several test cases that include:
    • Setup the test environment.
    • Test cases for each test scenario.
    • Reproducible steps and results.

Test

Unit Test Coverage

Package-wide coverage containing only the files affected by this PR is available here. Save and open in your browser.

Before: 3657a1d

core: 27.7%
core/vm: 40.5%
hmy: no test files
internal/params: no test files
node/worker: 26.8%
rosetta/services: 36.7%
staking: no test files
staking/types: 65.3%

After: 92cd2fd

core: 33.4%
core/vm: 43.2%
hmy: no test files
internal/params: no test files
node/worker: 27.1%
rosetta/services: 36.7%
staking: 88.0%
staking/types: 62.9%

Test/Run Logs

For the Go node, refer to this link which contains the test coverage for only the files modified by this PR; please save and open this in your browser.

For the smart contract staking library, I copy the logs below. These logs cover all the edge cases of staking, as requested in the bounty. The instructions to run these tests are in the repository README, and the edge cases are described here. These logs are as of MaxMustermann2/harmony-staking-precompiles@5d87fe9 and 92cd2fd.

==================================================================== test session starts =====================================================================
platform linux -- Python 3.9.2, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /usr/bin/python3
cachedir: .pytest_cache
hypothesis profile 'brownie-verbose' -> verbosity=2, deadline=None, max_examples=50, stateful_step_count=10, report_multiple_bugs=False, database=DirectoryBasedExampleDatabase(PosixPath('/home/user/.brownie/hypothesis'))
rootdir: /home/user/go/src/github.com/harmony-one/harmony-staking-precompiles
plugins: eth-brownie-1.17.2, forked-1.3.0, flaky-3.7.0, xdist-1.34.0, order-1.0.0, hypothesis-6.27.3, web3-5.25.0
collected 19 items                                                                                                                                           

tests/test_staking_contract.py::test_collect_rewards_fail_before_create_validator RUNNING
Setting up test environment
Waiting for node at http://localhost:9500 to boot up and reach staking era
Current block number = 70
Current epoch number = 8
Node is booted up
Main staking contract is at 0xa210f356046b9497E73581F0b8B38fa4988F913B
Set up test environment
Gas consumed is 48584
tests/test_staking_contract.py::test_collect_rewards_fail_before_create_validator PASSED
tests/test_staking_contract.py::test_delegate_fail_before_create_validator RUNNING
Gas consumed is 49435
tests/test_staking_contract.py::test_delegate_fail_before_create_validator PASSED
tests/test_staking_contract.py::test_undelegate_fail_before_create_validator RUNNING
Gas consumed is 49479
tests/test_staking_contract.py::test_undelegate_fail_before_create_validator PASSED
tests/test_staking_contract.py::test_delegate_fail_not_funded RUNNING
Creating the validator at one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3 using pyhmy
Gas consumed is 49435
tests/test_staking_contract.py::test_delegate_fail_not_funded PASSED
tests/test_staking_contract.py::test_delegate_fail_amount_lt_100 RUNNING
Funding the contract with 100 ONE
Gas consumed is 49435
tests/test_staking_contract.py::test_delegate_fail_amount_lt_100 PASSED
tests/test_staking_contract.py::test_delegate_fail_invalid_validator RUNNING
Gas consumed is 49435
tests/test_staking_contract.py::test_delegate_fail_invalid_validator PASSED
tests/test_staking_contract.py::test_delegate_success RUNNING
Gas consumed is 46259
tests/test_staking_contract.py::test_delegate_success PASSED
tests/test_staking_contract.py::test_undelegate_fail_invalid_delegator RUNNING
Gas consumed is 49479
tests/test_staking_contract.py::test_undelegate_fail_invalid_delegator PASSED
tests/test_staking_contract.py::test_undelegate_fail_amount_gt_delegated RUNNING
Gas consumed is 49491
tests/test_staking_contract.py::test_undelegate_fail_amount_gt_delegated PASSED
tests/test_staking_contract.py::test_collect_rewards_success RUNNING
Waiting for rewards to show up
We have rewards now
Gas consumed is 44936
tests/test_staking_contract.py::test_collect_rewards_success PASSED
tests/test_staking_contract.py::test_undelegate_success RUNNING
Gas consumed is 46303
tests/test_staking_contract.py::test_undelegate_success PASSED
tests/test_staking_contract.py::test_delegate_fail_malicious RUNNING
Gas consumed is 49774
tests/test_staking_contract.py::test_delegate_fail_malicious PASSED
tests/test_staking_contract.py::test_multiple_calls_success RUNNING
Making spare validators
Creating the validator at one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37 using pyhmy
Creating the validator at one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes using pyhmy
Deploying multiple calls contract
Multiple calls contract is at 0x56B8E61105E3589AAc8FaD6CF2430d1BC36451CE
Funding multiple calls contract
Calling delegateMultipleAndCollectRewards with 3 delegation(s)
Gas consumed is 125591
Calling switchDelegationAndCollectRewards
Gas consumed is 99004
Calling switchDelegationAndCollectRewards
Gas consumed is 102175
tests/test_staking_contract.py::test_multiple_calls_success PASSED
tests/test_staking_contract.py::test_many_calls_success RUNNING
Attempting 3094 precompile calls to check block time
Block #162 was produced in 2 seconds
Gas consumed is 79998458
tests/test_staking_contract.py::test_many_calls_success PASSED
tests/test_staking_contract.py::test_contract_which_reverts_success RUNNING
Deploying contract which reverts
Deployed contract which reverts
Funding the contract, which reverts, with 100 ONE
Attempting a delegate and revert
Delegations are unchanged
tests/test_staking_contract.py::test_contract_which_reverts_success PASSED
tests/test_staking_contract.py::test_eoa_access_success RUNNING
Gas consumed is 43836
tests/test_staking_contract.py::test_eoa_access_success PASSED
tests/test_staking_contract.py::test_eoa_subsidize_success RUNNING
Gas consumed is 141404
This is >= than 125000
tests/test_staking_contract.py::test_eoa_subsidize_success PASSED
tests/test_staking_contract.py::test_shard_1_fail RUNNING
Deploying contract on shard 1
Contract deployed at 0x2f2c626e41e1a8ae4c333bb62939bf32ededca9a on shard 1
Funding contract on shard 1
Confirming contract funding from shard 0
Confirming contract funding to shard 1
Delegating for contract on shard 1
As expected, delegation on shard 1 did not go through
Delegating for contract on shard 0
Confirming contract funding on shard 0
Delegation on shard 0 went through
tests/test_staking_contract.py::test_shard_1_fail PASSED
tests/test_staking_contract.py::test_eoa_migrate RUNNING
Gas consumed is 42800
tests/test_staking_contract.py::test_eoa_migrate PASSED

=============================================================== 19 passed in 240.88s (0:04:00) ===============================================================

Operational Checklist

  1. Does this PR introduce backward-incompatible changes to the on-disk data structure and/or the over-the-wire protocol?. (If no, skip to question 8.)
    No.

  2. Describe the migration plan.. For each flag epoch, describe what changes take place at the flag epoch, the anticipated interactions between upgraded/non-upgraded nodes, and any special operational considerations for the migration.

  3. Describe how the plan was tested.

  4. How much minimum baking period after the last flag epoch should we allow on Pangaea before promotion onto mainnet?

  5. What are the planned flag epoch numbers and their ETAs on Pangaea?

  6. What are the planned flag epoch numbers and their ETAs on mainnet?
    Note that this must be enough to cover baking period on Pangaea.

  7. What should node operators know about this planned change?

  8. Does this PR introduce backward-incompatible changes NOT related to on-disk data structure and/or over-the-wire protocol? (If no, continue to question 11.)
    No.

  9. Does the existing node.sh continue to work with this change?

  10. What should node operators know about this change?
    Smart contracts can now use the staking functions of the chain.

  11. Does this PR introduce significant changes to the operational requirements of the node software, such as >20% increase in CPU, memory, and/or disk usage?
    No.

Create write capable precompiles that can perform staking transactions
Add hard fork logic (EpochTBD) for these precompiles
Tests for new code with at least 80% unit test coverage
Staking library + tests in MaxMustermann2/harmony-staking-precompiles
From Solidity, use abi.encodeWithSelector and match it against the
exact ABI of the functions. This allows us to remove the need for
a directive (32) being encoded, and thus saves 28 bytes of data.
core/staking_verifier.go Outdated Show resolved Hide resolved
core/state_processor.go Outdated Show resolved Hide resolved
MaxMustermann2 added a commit to MaxMustermann2/harmony-staking-precompiles that referenced this pull request Oct 29, 2021
Since smart contracts can no longer beecome validators,
this field is superfluous. Remove it from the Wrapper
structure, and do not assign it a value when creating
a validator. Build and goimports checked
@JackyWYX
Copy link
Contributor

Looks very good. Could you please add a test case of multiple delegations/undelegations/claimReward in a single transaction to see whether the result is expected?

Copy link
Contributor

@JackyWYX JackyWYX left a comment

Choose a reason for hiding this comment

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

This PR is not safe to merge because the revert mechanism is broken for statedb.UpdateValidatorWrapper. We need to implement that first so that this PR change can be merged. I am truly sorry for the inconvenience. @rlan35 Any thought on reverting UpdateValidatorWrapper? Another bounty maybe?

core/vm/contracts_write.go Outdated Show resolved Hide resolved
core/vm/contracts_write.go Show resolved Hide resolved
core/vm/contracts_write.go Outdated Show resolved Hide resolved
core/vm/contracts_write.go Outdated Show resolved Hide resolved
@@ -45,12 +45,20 @@ type (
// GetVRFFunc returns the nth block vrf in the blockchain
// and is used by the precompile VRF contract.
GetVRFFunc func(uint64) common.Hash
// Below functions are used by staking precompile / state transition
CreateValidatorFunc func(db StateDB, stakeMsg *stakingTypes.CreateValidator) error
Copy link
Contributor

Choose a reason for hiding this comment

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

We may also remove CreateValidator and EditValidator

Copy link
Contributor

Choose a reason for hiding this comment

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

why removing create and edit validator ? that would be useful for the node runner to manage their validator via metamask

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We may also remove CreateValidator and EditValidator

These cannot be removed because they are used in state_transition.go as err = st.evm.CreateValidator(st.evm.StateDB, stkMsg) and err = st.evm.EditValidator(st.evm.StateDB, stkMsg); and the types need to be defined for use in the evm object.

(1) Comments to start with function names
(2) Comments for public variables
(3) Comment to match function name RunPrecompiledContract
(4) Clarify that CreateValidatorFunc + EditValidatorFunc are still used
@MaxMustermann2
Copy link
Contributor Author

MaxMustermann2 commented Nov 17, 2021

Looks very good. Could you please add a test case of multiple delegations/undelegations/claimReward in a single transaction to see whether the result is expected?

This has been added, please look at test_multiple_calls_success in the PR description.

This PR is not safe to merge because the revert mechanism is broken for statedb.UpdateValidatorWrapper. We need to implement that first so that this PR change can be merged. I am truly sorry for the inconvenience. @rlan35 Any thought on reverting UpdateValidatorWrapper? Another bounty maybe?

Fair point, I would be happy to work on the bounty if / when it is made.

@JackyWYX
Copy link
Contributor

JackyWYX commented Nov 17, 2021

The bounty of revert mechanism for UpdateValidatorWrapper is here: harmony-one/bounties#90

@shimmyshine
Copy link

Nothing of major value to add other than I am very eagerly looking forward to seeing this implemented!

@MaxMustermann2
Copy link
Contributor Author

As discussed with @rlan35 over Discord, I have added a staking migration precompile as well as one to fetch the current epoch. The logs for the integration tests are updated in the top comment, and the test coverage for files modified by this PR has been updated here.

Regarding the failed build, I believe the reason is that the one on main is failing as well. I tested the rpc checks locally and they seem to work for me. I may be wrong though, so I will investigate again when main is fixed.

blockNum := block.Number()
for _, stakeMsg := range stakeMsgs {
Copy link
Contributor

Choose a reason for hiding this comment

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

StakeMsgs are from Normal Transactions which is processed before Staking transaction. So this change is still consistent with the ordering of the processing. Right? No non-deterministic result here.

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, but you are right that it needs to be thoroughly tested. I would appreciate it if this change spends a bit of time on testnet so I (and others) can verify the behaviour.

Choose a reason for hiding this comment

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

Looking forward to testing on testnet. Nice work so far.

core/staking_verifier.go Outdated Show resolved Hide resolved
core/vm/contracts.go Outdated Show resolved Hide resolved
core/vm/contracts_write.go Outdated Show resolved Hide resolved
core/vm/gas.go Show resolved Hide resolved
Merge the two precompiles into one, add gas calculation for migration
precompile. Move epoch precompile to 251 as a result. When migrating,
add undelegations to `To`'s existing undelegations, if any match the
epoch.
core/evm.go Show resolved Hide resolved
core/vm/contracts_write.go Outdated Show resolved Hide resolved
core/vm/contracts_write.go Outdated Show resolved Hide resolved
staking/precompile.go Outdated Show resolved Hide resolved
In response to review comments, add tests for migration gas wherein
there are 0/1/2 delegations to migrate. Add the index out of bound check
to migration gas calculator and remove panics. Lastly, re-sort
migrated undelegations if no existing undelegation in the same epoch was
found on `To`.
core/staking_verifier.go Outdated Show resolved Hide resolved
core/vm/evm.go Show resolved Hide resolved
@rlan35 rlan35 merged commit d500c4e into harmony-one:main Jan 21, 2022
@sijad
Copy link

sijad commented Jan 21, 2022

is there any estimate for the epoch that enables this?

@polymorpher
Copy link

The amount (e.g. for CollectReward) in the log emitted is using a non-fixed length integer. This will make it difficult for the client (e.g. web3 libraries) to parse the log (and capture the value) because it would be expecting fixed length integers (e.g. uint256, uint64, etc.)

Data: totalRewards.Bytes(),

@polymorpher
Copy link

Also the event's topic hash is not following the standard way:

https://github.com/harmony-one/harmony/blob/027896adacb8d011e57c0c0e65b47d63327f282b/staking/params.go

It is computed from the hash of the name only. The standard way is the full signature, including parameter types (e.g. keccak256(Harmony/CollectRewards(uint256))

@MaxMustermann2
Copy link
Contributor Author

MaxMustermann2 commented Mar 28, 2022

This was not changed by me; I moved the code from state_transition.go to here.

st.state.AddLog(&types.Log{
Address: collectRewards.DelegatorAddress,
Topics: []common.Hash{staking2.CollectRewardsTopic},
Data: totalRewards.Bytes(),
BlockNumber: st.evm.BlockNumber.Uint64(),
})

@rlan35 RJ, can you comment if this is something that can be changed ? For consistency with prior events, it should be changed with a fork. We can replace totalRewards.Bytes() with common.LeftPadBytes(totalRewards.Bytes(), 32).

@polymorpher Can you please tell me what this is needed for, and if the proposed read only staking precompile can help instead?

@polymorpher
Copy link

It's for client side apps to check how much reward was collected in a particular transaction. See polymorpher/one-wallet#251 (comment) for a visual output. My current workaround is to manually pad the data when event name equals a pre-determined one.

polymorpher/one-wallet@a2f9245

Also I am computing the expected topic hash of precompiled events in a special manner

polymorpher/one-wallet@1b9bb45

@MaxMustermann2
Copy link
Contributor Author

Thanks, this will be updated when the read-only staking precompile is merged. I will give you a heads up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants