mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2025-01-03 00:27:38 +00:00
Update Registration Contract in Solidity (#996)
This commit is contained in:
parent
c5d4c27989
commit
b8041e3dca
@ -8,6 +8,8 @@
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"security/no-inline-assembly": ["warning"],
|
||||
|
||||
"indentation": [
|
||||
"error",
|
||||
4
|
||||
|
@ -1,10 +1,23 @@
|
||||
## Validator Registration Contract
|
||||
|
||||
For beacon chain design, a validator will deposit 32 ETH to the main chain smart contract.
|
||||
A validator will deposit 32 ETH to the registration
|
||||
contract. The contract will generate a receipt showing the validator as a
|
||||
qualified validator.
|
||||
The deposit is considered to be burned. As you burn the 32 ETH to participate,
|
||||
the beacon chain will see it and will credit the validator with the validator bond,
|
||||
and the validator can begin to validate. At some point in the future, after a hard fork,
|
||||
the original deposit + interest can be withdrawn back on one of the shards.
|
||||
At some point in the future, after a hard fork,
|
||||
the original deposit + interest can be withdrawn back on one of the shards.
|
||||
To call the `registration` function, it takes arguments of `pubkey`,
|
||||
`proof_of_possession`, `withdrawal_credentials` and `randao_commitment`.
|
||||
If the user wants to deposit more than `DEPOSIT_SIZE` ETH, they would
|
||||
need to make multiple `registration` calls.
|
||||
When the contract publishes the `ChainStart` log, beacon nodes will
|
||||
start off the beacon chain with slot 0 and last recorded `block.timestamp`
|
||||
as beacon chain genesis time.
|
||||
The registration contract generate receipts with the various arguments
|
||||
for consumption by beacon nodes. It does not validate `proof_of_possession`
|
||||
and `withdrawal_credentials`, pushing the validation logic to the
|
||||
beacon chain.
|
||||
|
||||
## How to execute tests
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,48 +1,111 @@
|
||||
pragma solidity 0.4.23;
|
||||
pragma solidity ^0.4.23;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
|
||||
contract ValidatorRegistration {
|
||||
event ValidatorRegistered(
|
||||
bytes32 indexed hashedPubkey,
|
||||
uint256 withdrawalShardID,
|
||||
address indexed withdrawalAddressbytes32,
|
||||
bytes32 indexed randaoCommitment
|
||||
|
||||
event HashChainValue(
|
||||
bytes indexed previousReceiptRoot,
|
||||
bytes data,
|
||||
uint totalDepositcount
|
||||
);
|
||||
|
||||
mapping (bytes32 => bool) public usedHashedPubkey;
|
||||
event ChainStart(
|
||||
bytes indexed receiptRoot,
|
||||
bytes time
|
||||
);
|
||||
|
||||
uint public constant VALIDATOR_DEPOSIT = 32 ether;
|
||||
uint public constant DEPOSIT_SIZE = 32 ether;
|
||||
uint public constant DEPOSITS_FOR_CHAIN_START = 8;
|
||||
uint public constant MIN_TOPUP_SIZE = 1 ether;
|
||||
uint public constant GWEI_PER_ETH = 10 ** 9;
|
||||
// Setting MERKLE_TREE_DEPTH to 16 instead of 32 due to gas limit
|
||||
uint public constant MERKLE_TREE_DEPTH = 16;
|
||||
uint public constant SECONDS_PER_DAY = 86400;
|
||||
|
||||
// Validator registers by sending a transaction of 32ETH to
|
||||
// the following deposit function. The deposit function takes in
|
||||
// validator's public key, withdrawal shard ID (which shard
|
||||
// to send the deposit back to), withdrawal address (which address
|
||||
// to send the deposit back to) and randao commitment.
|
||||
mapping (uint => bytes) public receiptTree;
|
||||
uint public totalDepositCount;
|
||||
|
||||
// When users wish to become a validator by moving ETH from
|
||||
// 1.0 chian to the 2.0 chain, they should call this function
|
||||
// sending along DEPOSIT_SIZE ETH and providing depositParams
|
||||
// as a simple serialize'd DepositParams object of the following
|
||||
// form:
|
||||
// {
|
||||
// 'pubkey': 'int256',
|
||||
// 'proof_of_possession': ['int256'],
|
||||
// 'withdrawal_credentials`: 'hash32',
|
||||
// 'randao_commitment`: 'hash32'
|
||||
// }
|
||||
function deposit(
|
||||
bytes _pubkey,
|
||||
uint _withdrawalShardID,
|
||||
address _withdrawalAddressbytes32,
|
||||
bytes32 _randaoCommitment
|
||||
bytes depositParams
|
||||
)
|
||||
public
|
||||
payable
|
||||
{
|
||||
uint index = totalDepositCount + 2 ** MERKLE_TREE_DEPTH;
|
||||
bytes memory msgGweiInBytes = toBytes(msg.value);
|
||||
bytes memory timeStampInBytes = toBytes(block.timestamp);
|
||||
bytes memory depositData = mergeBytes(mergeBytes(msgGweiInBytes, timeStampInBytes), depositParams);
|
||||
|
||||
emit HashChainValue(receiptTree[1], depositParams, totalDepositCount);
|
||||
|
||||
receiptTree[index] = abi.encodePacked(keccak256(depositData));
|
||||
for (uint i = 0; i < MERKLE_TREE_DEPTH; i++) {
|
||||
index = index / 2;
|
||||
receiptTree[index] = abi.encodePacked(keccak256(mergeBytes(receiptTree[index * 2], receiptTree[index * 2 + 1])));
|
||||
}
|
||||
|
||||
require(
|
||||
msg.value == VALIDATOR_DEPOSIT,
|
||||
"Incorrect validator deposit"
|
||||
msg.value <= DEPOSIT_SIZE,
|
||||
"Deposit can't be greater than DEPOSIT_SIZE."
|
||||
);
|
||||
require(
|
||||
_pubkey.length == 48,
|
||||
"Public key is not 48 bytes"
|
||||
msg.value >= MIN_TOPUP_SIZE,
|
||||
"Deposit can't be lesser than MIN_TOPUP_SIZE."
|
||||
);
|
||||
if (msg.value == DEPOSIT_SIZE) {
|
||||
totalDepositCount++;
|
||||
}
|
||||
|
||||
bytes32 hashedPubkey = keccak256(abi.encodePacked(_pubkey));
|
||||
require(
|
||||
!usedHashedPubkey[hashedPubkey],
|
||||
"Public key already used"
|
||||
);
|
||||
// When ChainStart log publishes, beacon chain node initializes the chain and use timestampDayBoundry
|
||||
// as genesis time.
|
||||
if (totalDepositCount == DEPOSITS_FOR_CHAIN_START) {
|
||||
uint timestampDayBoundry = block.timestamp - block.timestamp % SECONDS_PER_DAY + SECONDS_PER_DAY;
|
||||
bytes memory timestampDayBoundryBytes = toBytes(timestampDayBoundry);
|
||||
emit ChainStart(receiptTree[1], timestampDayBoundryBytes);
|
||||
}
|
||||
}
|
||||
|
||||
usedHashedPubkey[hashedPubkey] = true;
|
||||
function getReceiptRoot() public constant returns (bytes) {
|
||||
return receiptTree[1];
|
||||
}
|
||||
|
||||
emit ValidatorRegistered(hashedPubkey, _withdrawalShardID, _withdrawalAddressbytes32, _randaoCommitment);
|
||||
function toBytes(uint x) private constant returns (bytes memory) {
|
||||
bytes memory b = new bytes(32);
|
||||
assembly { mstore(add(b, 32), x) }
|
||||
return b;
|
||||
}
|
||||
|
||||
function mergeBytes(bytes memory a, bytes memory b) private returns (bytes memory c) {
|
||||
// Store the length of the first array
|
||||
uint alen = a.length;
|
||||
// Store the length of BOTH arrays
|
||||
uint totallen = alen + b.length;
|
||||
// Count the loops required for array a (sets of 32 bytes)
|
||||
uint loopsa = (a.length + 31) / 32;
|
||||
// Count the loops required for array b (sets of 32 bytes)
|
||||
uint loopsb = (b.length + 31) / 32;
|
||||
assembly {
|
||||
let m := mload(0x40)
|
||||
// Load the length of both arrays to the head of the new bytes array
|
||||
mstore(m, totallen)
|
||||
// Add the contents of a to the array
|
||||
for { let i := 0 } lt(i, loopsa) { i := add(1, i) } { mstore(add(m, mul(32, add(1, i))), mload(add(a, mul(32, add(1, i))))) }
|
||||
// Add the contents of b to the array
|
||||
for { let i := 0 } lt(i, loopsb) { i := add(1, i) } { mstore(add(m, add(mul(32, add(1, i)), alen)), mload(add(b, mul(32, add(1, i))))) }
|
||||
mstore(0x40, add(m, add(32, totallen)))
|
||||
c := m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package vrc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -15,19 +16,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
amount33Eth, _ = new(big.Int).SetString("33000000000000000000", 10)
|
||||
amount32Eth, _ = new(big.Int).SetString("32000000000000000000", 10)
|
||||
amount31Eth, _ = new(big.Int).SetString("31000000000000000000", 10)
|
||||
amount33Eth, _ = new(big.Int).SetString("33000000000000000000", 10)
|
||||
amount32Eth, _ = new(big.Int).SetString("32000000000000000000", 10)
|
||||
amountLessThan1Eth, _ = new(big.Int).SetString("500000000000000000", 10)
|
||||
)
|
||||
|
||||
type testAccount struct {
|
||||
addr common.Address
|
||||
withdrawalAddress common.Address
|
||||
randaoCommitment [32]byte
|
||||
pubKey []byte
|
||||
contract *ValidatorRegistration
|
||||
backend *backends.SimulatedBackend
|
||||
txOpts *bind.TransactOpts
|
||||
addr common.Address
|
||||
contract *ValidatorRegistration
|
||||
backend *backends.SimulatedBackend
|
||||
txOpts *bind.TransactOpts
|
||||
}
|
||||
|
||||
func setup() (*testAccount, error) {
|
||||
@ -45,7 +43,7 @@ func setup() (*testAccount, error) {
|
||||
|
||||
addr := crypto.PubkeyToAddress(privKey.PublicKey)
|
||||
txOpts := bind.NewKeyedTransactor(privKey)
|
||||
startingBalance, _ := new(big.Int).SetString("100000000000000000000", 10)
|
||||
startingBalance, _ := new(big.Int).SetString("1000000000000000000000", 10)
|
||||
genesis[addr] = core.GenesisAccount{Balance: startingBalance}
|
||||
backend := backends.NewSimulatedBackend(genesis, 2100000)
|
||||
|
||||
@ -54,7 +52,7 @@ func setup() (*testAccount, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &testAccount{addr, common.Address{}, [32]byte{}, pubKey, contract, backend, txOpts}, nil
|
||||
return &testAccount{addr, contract, backend, txOpts}, nil
|
||||
}
|
||||
|
||||
func TestSetupAndContractRegistration(t *testing.T) {
|
||||
@ -64,100 +62,58 @@ func TestSetupAndContractRegistration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// negative test case, public key that is not 48 bytes.
|
||||
func TestRegisterWithLessThan48BytesPubkey(t *testing.T) {
|
||||
// negative test case, deposit with less than 1 ETH which is less than the top off amount.
|
||||
func TestRegisterWithLessThan1Eth(t *testing.T) {
|
||||
testAccount, err := setup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var pubKey = make([]byte, 32)
|
||||
copy(pubKey, testAccount.pubKey[:])
|
||||
withdrawAddr := &common.Address{'A', 'D', 'D', 'R', 'E', 'S', 'S'}
|
||||
randaoCommitment := &[32]byte{'S', 'H', 'H', 'H', 'H', 'I', 'T', 'S', 'A', 'S', 'E', 'C', 'R', 'E', 'T'}
|
||||
|
||||
testAccount.txOpts.Value = amount32Eth
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, pubKey, big.NewInt(0), *withdrawAddr, *randaoCommitment)
|
||||
if err == nil {
|
||||
t.Error("Validator registration should have failed with a 32 bytes pubkey")
|
||||
}
|
||||
}
|
||||
|
||||
// negative test case, deposit with less than 32 ETH.
|
||||
func TestRegisterWithLessThan32Eth(t *testing.T) {
|
||||
testAccount, err := setup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withdrawAddr := &common.Address{'A', 'D', 'D', 'R', 'E', 'S', 'S'}
|
||||
randaoCommitment := &[32]byte{'S', 'H', 'H', 'H', 'H', 'I', 'T', 'S', 'A', 'S', 'E', 'C', 'R', 'E', 'T'}
|
||||
|
||||
testAccount.txOpts.Value = amount31Eth
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, testAccount.pubKey, big.NewInt(0), *withdrawAddr, *randaoCommitment)
|
||||
testAccount.txOpts.Value = amountLessThan1Eth
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{})
|
||||
if err == nil {
|
||||
t.Error("Validator registration should have failed with insufficient deposit")
|
||||
}
|
||||
}
|
||||
|
||||
// negative test case, deposit more than 32 ETH.
|
||||
// negative test case, deposit with more than 32 ETH which is more than the asked amount.
|
||||
func TestRegisterWithMoreThan32Eth(t *testing.T) {
|
||||
testAccount, err := setup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withdrawAddr := &common.Address{'A', 'D', 'D', 'R', 'E', 'S', 'S'}
|
||||
randaoCommitment := &[32]byte{'S', 'H', 'H', 'H', 'H', 'I', 'T', 'S', 'A', 'S', 'E', 'C', 'R', 'E', 'T'}
|
||||
|
||||
testAccount.txOpts.Value = amount33Eth
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, testAccount.pubKey, big.NewInt(0), *withdrawAddr, *randaoCommitment)
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{})
|
||||
if err == nil {
|
||||
t.Error("Validator registration should have failed with more than deposit amount")
|
||||
t.Error("Validator registration should have failed with more than asked deposit amount")
|
||||
}
|
||||
}
|
||||
|
||||
// negative test case, test registering with the same public key twice.
|
||||
func TestRegisterTwice(t *testing.T) {
|
||||
// normal test case, test depositing 32 ETH and verify HashChainValue event is correctly emitted.
|
||||
func TestValidatorRegisters(t *testing.T) {
|
||||
testAccount, err := setup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withdrawAddr := &common.Address{'A', 'D', 'D', 'R', 'E', 'S', 'S'}
|
||||
randaoCommitment := &[32]byte{'S', 'H', 'H', 'H', 'H', 'I', 'T', 'S', 'A', 'S', 'E', 'C', 'R', 'E', 'T'}
|
||||
|
||||
testAccount.txOpts.Value = amount32Eth
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, testAccount.pubKey, big.NewInt(0), *withdrawAddr, *randaoCommitment)
|
||||
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{'A'})
|
||||
testAccount.backend.Commit()
|
||||
if err != nil {
|
||||
t.Errorf("Validator registration failed: %v", err)
|
||||
}
|
||||
|
||||
testAccount.txOpts.Value = amount32Eth
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, testAccount.pubKey, big.NewInt(0), *withdrawAddr, *randaoCommitment)
|
||||
testAccount.backend.Commit()
|
||||
if err == nil {
|
||||
t.Errorf("Registration should have failed with same public key twice")
|
||||
}
|
||||
}
|
||||
|
||||
// normal test case, test depositing 32 ETH and verify validatorRegistered event is correctly emitted.
|
||||
func TestRegister(t *testing.T) {
|
||||
testAccount, err := setup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withdrawAddr := &common.Address{'A', 'D', 'D', 'R', 'E', 'S', 'S'}
|
||||
randaoCommitment := &[32]byte{'S', 'H', 'H', 'H', 'H', 'I', 'T', 'S', 'A', 'S', 'E', 'C', 'R', 'E', 'T'}
|
||||
shardID := big.NewInt(99)
|
||||
testAccount.txOpts.Value = amount32Eth
|
||||
|
||||
var hashedPub [32]byte
|
||||
copy(hashedPub[:], crypto.Keccak256(testAccount.pubKey))
|
||||
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, testAccount.pubKey, shardID, *withdrawAddr, *randaoCommitment)
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{'B'})
|
||||
testAccount.backend.Commit()
|
||||
if err != nil {
|
||||
t.Errorf("Validator registration failed: %v", err)
|
||||
}
|
||||
log, err := testAccount.contract.FilterValidatorRegistered(&bind.FilterOpts{}, [][32]byte{}, []common.Address{}, [][32]byte{})
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{'C'})
|
||||
testAccount.backend.Commit()
|
||||
if err != nil {
|
||||
t.Errorf("Validator registration failed: %v", err)
|
||||
}
|
||||
log, err := testAccount.contract.FilterHashChainValue(&bind.FilterOpts{}, [][]byte{})
|
||||
|
||||
defer func() {
|
||||
err = log.Close()
|
||||
@ -173,16 +129,65 @@ func TestRegister(t *testing.T) {
|
||||
t.Fatal(log.Error())
|
||||
}
|
||||
log.Next()
|
||||
if log.Event.WithdrawalShardID.Cmp(shardID) != 0 {
|
||||
t.Errorf("validatorRegistered event withdrawal shard ID miss matched. Want: %v, Got: %v", shardID, log.Event.WithdrawalShardID)
|
||||
|
||||
if log.Event.TotalDepositcount.Cmp(big.NewInt(0)) != 0 {
|
||||
t.Errorf("HashChainValue event total desposit count miss matched. Want: %v, Got: %v", big.NewInt(0), log.Event.TotalDepositcount)
|
||||
}
|
||||
if log.Event.RandaoCommitment != *randaoCommitment {
|
||||
t.Errorf("validatorRegistered event randao commitment miss matched. Want: %v, Got: %v", *randaoCommitment, log.Event.RandaoCommitment)
|
||||
if !bytes.Equal(log.Event.Data, []byte{'A'}) {
|
||||
t.Errorf("validatorRegistered event randao commitment miss matched. Want: %v, Got: %v", []byte{'A'}, log.Event.Data)
|
||||
}
|
||||
if log.Event.HashedPubkey != hashedPub {
|
||||
t.Errorf("validatorRegistered event public key miss matched. Want: %v, Got: %v", common.BytesToHash(testAccount.pubKey), log.Event.HashedPubkey)
|
||||
|
||||
log.Next()
|
||||
if log.Event.TotalDepositcount.Cmp(big.NewInt(1)) != 0 {
|
||||
t.Errorf("HashChainValue event total desposit count miss matched. Want: %v, Got: %v", big.NewInt(1), log.Event.TotalDepositcount)
|
||||
}
|
||||
if log.Event.WithdrawalAddressbytes32 != *withdrawAddr {
|
||||
t.Errorf("validatorRegistered event withdrawal address miss matched. Want: %v, Got: %v", *withdrawAddr, log.Event.WithdrawalAddressbytes32)
|
||||
if !bytes.Equal(log.Event.Data, []byte{'B'}) {
|
||||
t.Errorf("validatorRegistered event randao commitment miss matched. Want: %v, Got: %v", []byte{'B'}, log.Event.Data)
|
||||
}
|
||||
|
||||
log.Next()
|
||||
if log.Event.TotalDepositcount.Cmp(big.NewInt(2)) != 0 {
|
||||
t.Errorf("HashChainValue event total desposit count miss matched. Want: %v, Got: %v", big.NewInt(1), log.Event.TotalDepositcount)
|
||||
}
|
||||
if !bytes.Equal(log.Event.Data, []byte{'C'}) {
|
||||
t.Errorf("validatorRegistered event randao commitment miss matched. Want: %v, Got: %v", []byte{'B'}, log.Event.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// normal test case, test beacon chain start log event.
|
||||
func TestChainStarts(t *testing.T) {
|
||||
testAccount, err := setup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testAccount.txOpts.Value = amount32Eth
|
||||
|
||||
for i := 0; i < 9; i++ {
|
||||
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{'A'})
|
||||
testAccount.backend.Commit()
|
||||
if err != nil {
|
||||
t.Errorf("Validator registration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log, err := testAccount.contract.FilterChainStart(&bind.FilterOpts{}, [][]byte{})
|
||||
|
||||
defer func() {
|
||||
err = log.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if log.Error() != nil {
|
||||
t.Fatal(log.Error())
|
||||
}
|
||||
log.Next()
|
||||
|
||||
if len(log.Event.Time) == 0 {
|
||||
t.Error("Chain start even did not get emitted, The start time is empty")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user