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", "error",
"double" "double"
], ],
"security/no-inline-assembly": ["warning"],
"indentation": [ "indentation": [
"error", "error",
4 4

View File

@ -1,10 +1,23 @@
## Validator Registration Contract ## 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 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, 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. 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 ## 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 { contract ValidatorRegistration {
event ValidatorRegistered(
bytes32 indexed hashedPubkey, event HashChainValue(
uint256 withdrawalShardID, bytes indexed previousReceiptRoot,
address indexed withdrawalAddressbytes32, bytes data,
bytes32 indexed randaoCommitment 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 mapping (uint => bytes) public receiptTree;
// the following deposit function. The deposit function takes in uint public totalDepositCount;
// validator's public key, withdrawal shard ID (which shard
// to send the deposit back to), withdrawal address (which address // When users wish to become a validator by moving ETH from
// to send the deposit back to) and randao commitment. // 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( function deposit(
bytes _pubkey, bytes depositParams
uint _withdrawalShardID,
address _withdrawalAddressbytes32,
bytes32 _randaoCommitment
) )
public public
payable 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( require(
msg.value == VALIDATOR_DEPOSIT, msg.value <= DEPOSIT_SIZE,
"Incorrect validator deposit" "Deposit can't be greater than DEPOSIT_SIZE."
); );
require( require(
_pubkey.length == 48, msg.value >= MIN_TOPUP_SIZE,
"Public key is not 48 bytes" "Deposit can't be lesser than MIN_TOPUP_SIZE."
); );
if (msg.value == DEPOSIT_SIZE) {
totalDepositCount++;
}
bytes32 hashedPubkey = keccak256(abi.encodePacked(_pubkey)); // When ChainStart log publishes, beacon chain node initializes the chain and use timestampDayBoundry
require( // as genesis time.
!usedHashedPubkey[hashedPubkey], if (totalDepositCount == DEPOSITS_FOR_CHAIN_START) {
"Public key already used" 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 package vrc
import ( import (
"bytes"
"crypto/ecdsa" "crypto/ecdsa"
"fmt" "fmt"
"log" "log"
@ -15,19 +16,16 @@ import (
) )
var ( var (
amount33Eth, _ = new(big.Int).SetString("33000000000000000000", 10) amount33Eth, _ = new(big.Int).SetString("33000000000000000000", 10)
amount32Eth, _ = new(big.Int).SetString("32000000000000000000", 10) amount32Eth, _ = new(big.Int).SetString("32000000000000000000", 10)
amount31Eth, _ = new(big.Int).SetString("31000000000000000000", 10) amountLessThan1Eth, _ = new(big.Int).SetString("500000000000000000", 10)
) )
type testAccount struct { type testAccount struct {
addr common.Address addr common.Address
withdrawalAddress common.Address contract *ValidatorRegistration
randaoCommitment [32]byte backend *backends.SimulatedBackend
pubKey []byte txOpts *bind.TransactOpts
contract *ValidatorRegistration
backend *backends.SimulatedBackend
txOpts *bind.TransactOpts
} }
func setup() (*testAccount, error) { func setup() (*testAccount, error) {
@ -45,7 +43,7 @@ func setup() (*testAccount, error) {
addr := crypto.PubkeyToAddress(privKey.PublicKey) addr := crypto.PubkeyToAddress(privKey.PublicKey)
txOpts := bind.NewKeyedTransactor(privKey) txOpts := bind.NewKeyedTransactor(privKey)
startingBalance, _ := new(big.Int).SetString("100000000000000000000", 10) startingBalance, _ := new(big.Int).SetString("1000000000000000000000", 10)
genesis[addr] = core.GenesisAccount{Balance: startingBalance} genesis[addr] = core.GenesisAccount{Balance: startingBalance}
backend := backends.NewSimulatedBackend(genesis, 2100000) backend := backends.NewSimulatedBackend(genesis, 2100000)
@ -54,7 +52,7 @@ func setup() (*testAccount, error) {
return nil, err 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) { func TestSetupAndContractRegistration(t *testing.T) {
@ -64,100 +62,58 @@ func TestSetupAndContractRegistration(t *testing.T) {
} }
} }
// negative test case, public key that is not 48 bytes. // negative test case, deposit with less than 1 ETH which is less than the top off amount.
func TestRegisterWithLessThan48BytesPubkey(t *testing.T) { func TestRegisterWithLessThan1Eth(t *testing.T) {
testAccount, err := setup() testAccount, err := setup()
if err != nil { if err != nil {
t.Fatal(err) 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 testAccount.txOpts.Value = amountLessThan1Eth
_, err = testAccount.contract.Deposit(testAccount.txOpts, pubKey, big.NewInt(0), *withdrawAddr, *randaoCommitment) _, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{})
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)
if err == nil { if err == nil {
t.Error("Validator registration should have failed with insufficient deposit") 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) { func TestRegisterWithMoreThan32Eth(t *testing.T) {
testAccount, err := setup() testAccount, err := setup()
if err != nil { if err != nil {
t.Fatal(err) 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 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 { 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. // normal test case, test depositing 32 ETH and verify HashChainValue event is correctly emitted.
func TestRegisterTwice(t *testing.T) { func TestValidatorRegisters(t *testing.T) {
testAccount, err := setup() testAccount, err := setup()
if err != nil { if err != nil {
t.Fatal(err) 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 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() testAccount.backend.Commit()
if err != nil { if err != nil {
t.Errorf("Validator registration failed: %v", err) t.Errorf("Validator registration failed: %v", err)
} }
_, err = testAccount.contract.Deposit(testAccount.txOpts, []byte{'B'})
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)
testAccount.backend.Commit() testAccount.backend.Commit()
if err != nil { if err != nil {
t.Errorf("Validator registration failed: %v", err) 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() { defer func() {
err = log.Close() err = log.Close()
@ -173,16 +129,65 @@ func TestRegister(t *testing.T) {
t.Fatal(log.Error()) t.Fatal(log.Error())
} }
log.Next() 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 { if !bytes.Equal(log.Event.Data, []byte{'A'}) {
t.Errorf("validatorRegistered event randao commitment miss matched. Want: %v, Got: %v", *randaoCommitment, log.Event.RandaoCommitment) 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 { if !bytes.Equal(log.Event.Data, []byte{'B'}) {
t.Errorf("validatorRegistered event withdrawal address miss matched. Want: %v, Got: %v", *withdrawAddr, log.Event.WithdrawalAddressbytes32) 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")
} }
} }