Update Registration Contract in Solidity (#996)

This commit is contained in:
terence tsao 2018-11-30 07:12:23 -08:00 committed by GitHub
parent c5d4c27989
commit b8041e3dca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 579 additions and 200 deletions

View File

@ -8,6 +8,8 @@
"error",
"double"
],
"security/no-inline-assembly": ["warning"],
"indentation": [
"error",
4

View File

@ -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,
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

View File

@ -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
}
}
}

View File

@ -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")
}
}