From bcd93fee36ca13591af9b7e162f02e4cc70d49a2 Mon Sep 17 00:00:00 2001 From: Eduard S Date: Mon, 7 Sep 2020 18:09:52 +0200 Subject: [PATCH] Implement initial version of test.Client --- .gitignore | 1 + eth/auction.go | 20 ++ eth/ethereum.go | 28 +-- eth/rollup.go | 80 +++++--- go.mod | 1 + go.sum | 2 + synchronizer/synchronizer.go | 25 ++- test/ethclient.go | 346 +++++++++++++++++++++++++++++++---- test/ethclient_test.go | 224 ++++++++++++++++++++--- 9 files changed, 621 insertions(+), 106 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d165d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gotest.sh diff --git a/eth/auction.go b/eth/auction.go index 0c27854..0ea1778 100644 --- a/eth/auction.go +++ b/eth/auction.go @@ -171,6 +171,26 @@ type AuctionEvents struct { //nolint:structcheck HEZClaimed []AuctionEventHEZClaimed } +// NewAuctionEvents creates an empty AuctionEvents with the slices initialized. +func NewAuctionEvents() AuctionEvents { + return AuctionEvents{ + NewBid: make([]AuctionEventNewBid, 0), + NewSlotDeadline: make([]AuctionEventNewSlotDeadline, 0), + NewClosedAuctionSlots: make([]AuctionEventNewClosedAuctionSlots, 0), + NewOutbidding: make([]AuctionEventNewOutbidding, 0), + NewDonationAddress: make([]AuctionEventNewDonationAddress, 0), + NewBootCoordinator: make([]AuctionEventNewBootCoordinator, 0), + NewOpenAuctionSlots: make([]AuctionEventNewOpenAuctionSlots, 0), + NewAllocationRatio: make([]AuctionEventNewAllocationRatio, 0), + NewCoordinator: make([]AuctionEventNewCoordinator, 0), + CoordinatorUpdated: make([]AuctionEventCoordinatorUpdated, 0), + NewForgeAllocated: make([]AuctionEventNewForgeAllocated, 0), + NewMinBidEpoch: make([]AuctionEventNewMinBidEpoch, 0), + NewForge: make([]AuctionEventNewForge, 0), + HEZClaimed: make([]AuctionEventHEZClaimed, 0), + } +} + // AuctionInterface is the inteface to to Auction Smart Contract type AuctionInterface interface { // diff --git a/eth/ethereum.go b/eth/ethereum.go index 48290cb..711e4c9 100644 --- a/eth/ethereum.go +++ b/eth/ethereum.go @@ -18,9 +18,9 @@ import ( // EthereumInterface is the interface to Ethereum type EthereumInterface interface { - EthCurrentBlock() (*big.Int, error) - EthHeaderByNumber(context.Context, *big.Int) (*types.Header, error) - EthBlockByNumber(context.Context, *big.Int) (*common.Block, error) + EthCurrentBlock() (int64, error) + // EthHeaderByNumber(context.Context, *big.Int) (*types.Header, error) + EthBlockByNumber(context.Context, int64) (*common.Block, error) } var ( @@ -214,29 +214,33 @@ func (c *EthereumClient) waitReceipt(ctx context.Context, tx *types.Transaction, } // EthCurrentBlock returns the current block number in the blockchain -func (c *EthereumClient) EthCurrentBlock() (*big.Int, error) { +func (c *EthereumClient) EthCurrentBlock() (int64, error) { ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second) defer cancel() header, err := c.client.HeaderByNumber(ctx, nil) if err != nil { - return nil, err + return 0, err } - return header.Number, nil + return header.Number.Int64(), nil } // EthHeaderByNumber internally calls ethclient.Client HeaderByNumber -func (c *EthereumClient) EthHeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { - return c.client.HeaderByNumber(ctx, number) -} +// func (c *EthereumClient) EthHeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { +// return c.client.HeaderByNumber(ctx, number) +// } // EthBlockByNumber internally calls ethclient.Client BlockByNumber and returns *common.Block -func (c *EthereumClient) EthBlockByNumber(ctx context.Context, number *big.Int) (*common.Block, error) { - block, err := c.client.BlockByNumber(ctx, number) +func (c *EthereumClient) EthBlockByNumber(ctx context.Context, number int64) (*common.Block, error) { + blockNum := big.NewInt(number) + if number == 0 { + blockNum = nil + } + block, err := c.client.BlockByNumber(ctx, blockNum) if err != nil { return nil, err } b := &common.Block{ - EthBlockNum: block.Number().Uint64(), + EthBlockNum: block.Number().Int64(), Timestamp: time.Unix(int64(block.Time()), 0), Hash: block.Hash(), } diff --git a/eth/rollup.go b/eth/rollup.go index fb90c93..27f003c 100644 --- a/eth/rollup.go +++ b/eth/rollup.go @@ -5,10 +5,19 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/hermeznetwork/hermez-node/common" "github.com/hermeznetwork/hermez-node/utils" "github.com/iden3/go-iden3-crypto/babyjub" ) +const ( + // FeeIdxCoordinatorLen is the number of tokens the coordinator can use + // to collect fees (determines the number of tokens that the + // coordinator can collect fees from). This value is determined by the + // circuit. + FeeIdxCoordinatorLen = 64 +) + // RollupConstants are the constants of the Rollup Smart Contract type RollupConstants struct { // Maxim Deposit allowed @@ -41,21 +50,28 @@ type RollupVariables struct { // QueueStruct is the queue of L1Txs for a batch //nolint:structcheck type QueueStruct struct { - L1TxQueue [][]byte - CurrentIndex int64 + L1TxQueue []common.L1Tx TotalL1TxFee *big.Int } +// NewQueueStruct creates a new clear QueueStruct. +func NewQueueStruct() *QueueStruct { + return &QueueStruct{ + L1TxQueue: make([]common.L1Tx, 0), + TotalL1TxFee: big.NewInt(0), + } +} + // RollupState represents the state of the Rollup in the Smart Contract //nolint:structcheck,unused type RollupState struct { StateRoot *big.Int ExitRoots []*big.Int - ExiNullifierMap map[[256 / 8]byte]bool + ExitNullifierMap map[[256 / 8]byte]bool TokenList []ethCommon.Address TokenMap map[ethCommon.Address]bool - mapL1TxQueue map[int64]QueueStruct - LastLTxBatch int64 + MapL1TxQueue map[int64]*QueueStruct + LastL1L2Batch int64 CurrentToForgeL1TxsNum int64 LastToForgeL1TxsNum int64 CurrentIdx int64 @@ -63,9 +79,7 @@ type RollupState struct { // RollupEventL1UserTx is an event of the Rollup Smart Contract type RollupEventL1UserTx struct { - L1UserTx []byte - ToForgeL1TxsNum int64 - Position int + L1Tx common.L1Tx } // RollupEventAddToken is an event of the Rollup Smart Contract @@ -76,7 +90,8 @@ type RollupEventAddToken struct { // RollupEventForgeBatch is an event of the Rollup Smart Contract type RollupEventForgeBatch struct { - BatchNum int64 + BatchNum int64 + EthTxHash ethCommon.Hash } // RollupEventUpdateForgeL1Timeout is an event of the Rollup Smart Contract @@ -117,21 +132,35 @@ type RollupEvents struct { //nolint:structcheck Withdraw []RollupEventWithdraw } +// NewRollupEvents creates an empty RollupEvents with the slices initialized. +func NewRollupEvents() RollupEvents { + return RollupEvents{ + L1UserTx: make([]RollupEventL1UserTx, 0), + AddToken: make([]RollupEventAddToken, 0), + ForgeBatch: make([]RollupEventForgeBatch, 0), + UpdateForgeL1Timeout: make([]RollupEventUpdateForgeL1Timeout, 0), + UpdateFeeL1UserTx: make([]RollupEventUpdateFeeL1UserTx, 0), + UpdateFeeAddToken: make([]RollupEventUpdateFeeAddToken, 0), + UpdateTokenHez: make([]RollupEventUpdateTokenHez, 0), + Withdraw: make([]RollupEventWithdraw, 0), + } +} + // RollupForgeBatchArgs are the arguments to the ForgeBatch function in the Rollup Smart Contract //nolint:structcheck,unused type RollupForgeBatchArgs struct { - proofA [2]*big.Int - proofB [2][2]*big.Int - proofC [2]*big.Int - newLastIdx int64 - newStRoot *big.Int - newExitRoot *big.Int - // TODO: Replace compressedL1CoordinatorTx, l2TxsData, feeIdxCoordinator for vectors - compressedL1CoordinatorTx []byte - l2TxsData []byte - feeIdxCoordinator []byte - verifierIdx int64 - l1Batch bool + ProofA [2]*big.Int + ProofB [2][2]*big.Int + ProofC [2]*big.Int + NewLastIdx int64 + NewStRoot *big.Int + NewExitRoot *big.Int + L1CoordinatorTxs []*common.L1Tx + L2Txs []*common.L2Tx + FeeIdxCoordinator []common.Idx + // Circuit selector + VerifierIdx int64 + L1Batch bool } // RollupInterface is the inteface to to Rollup Smart Contract @@ -176,7 +205,7 @@ type RollupInterface interface { RollupConstants() (*RollupConstants, error) RollupEventsByBlock(blockNum int64) (*RollupEvents, *ethCommon.Hash, error) - RollupForgeBatchArgs(*types.Transaction) (*RollupForgeBatchArgs, error) + RollupForgeBatchArgs(ethCommon.Hash) (*RollupForgeBatchArgs, error) } // @@ -293,6 +322,11 @@ func (c *RollupClient) RollupEventsByBlock(blockNum int64) (*RollupEvents, *ethC } // RollupForgeBatchArgs returns the arguments used in a ForgeBatch call in the Rollup Smart Contract in the given transaction -func (c *RollupClient) RollupForgeBatchArgs(transaction *types.Transaction) (*RollupForgeBatchArgs, error) { +func (c *RollupClient) RollupForgeBatchArgs(ethTxHash ethCommon.Hash) (*RollupForgeBatchArgs, error) { + // tx := client.TransactionByHash(ethTxHash) -> types.Transaction + // txData := types.Transaction -> Data() + // m := abi.MethodById(txData) -> Method + // m.Inputs.Unpack(txData) -> Args + // client.TransactionReceipt()? return nil, errTODO } diff --git a/go.mod b/go.mod index 1a60acd..386b396 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/jinzhu/gorm v1.9.15 github.com/jmoiron/sqlx v1.2.0 github.com/lib/pq v1.8.0 + github.com/mitchellh/copystructure v1.0.0 github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 github.com/russross/meddler v1.0.0 github.com/stretchr/testify v1.6.1 diff --git a/go.sum b/go.sum index 1b76b97..31ff90c 100644 --- a/go.sum +++ b/go.sum @@ -399,6 +399,7 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -408,6 +409,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/synchronizer/synchronizer.go b/synchronizer/synchronizer.go index f695254..b297f65 100644 --- a/synchronizer/synchronizer.go +++ b/synchronizer/synchronizer.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "errors" - "math/big" "sync" ethCommon "github.com/ethereum/go-ethereum/common" @@ -50,7 +49,7 @@ type BlockData struct { // Status is returned by the Status method type Status struct { - CurrentBlock uint64 + CurrentBlock int64 CurrentBatch common.BatchNum CurrentForgerAddr ethCommon.Address NextForgerAddr ethCommon.Address @@ -82,15 +81,15 @@ func (s *Synchronizer) Sync() error { s.mux.Lock() defer s.mux.Unlock() - var lastStoredForgeL1TxsNum uint64 + var lastStoredForgeL1TxsNum int64 // TODO: Get this information from ethClient once it's implemented // for the moment we will get the latestblock - 20 as firstSavedBlock - latestBlock, err := s.ethClient.EthBlockByNumber(context.Background(), nil) + latestBlock, err := s.ethClient.EthBlockByNumber(context.Background(), 0) if err != nil { return err } - s.firstSavedBlock, err = s.ethClient.EthBlockByNumber(context.Background(), big.NewInt(int64(latestBlock.EthBlockNum-blocksToSync))) + s.firstSavedBlock, err = s.ethClient.EthBlockByNumber(context.Background(), latestBlock.EthBlockNum-blocksToSync) if err != nil { return err } @@ -107,7 +106,7 @@ func (s *Synchronizer) Sync() error { lastSavedBlock = s.firstSavedBlock } else { // Get the latest block we have in History DB from blockchain to detect a reorg - ethBlock, err := s.ethClient.EthBlockByNumber(context.Background(), big.NewInt(int64(lastSavedBlock.EthBlockNum))) + ethBlock, err := s.ethClient.EthBlockByNumber(context.Background(), lastSavedBlock.EthBlockNum) if err != nil { return err } @@ -135,10 +134,10 @@ func (s *Synchronizer) Sync() error { return err } - log.Debugf("Blocks to sync: %v (lastSavedBlock: %v, latestBlock: %v)", latestBlockNum.Uint64()-lastSavedBlock.EthBlockNum, lastSavedBlock.EthBlockNum, latestBlockNum) + log.Debugf("Blocks to sync: %v (lastSavedBlock: %v, latestBlock: %v)", latestBlockNum-lastSavedBlock.EthBlockNum, lastSavedBlock.EthBlockNum, latestBlockNum) - for lastSavedBlock.EthBlockNum < latestBlockNum.Uint64() { - ethBlock, err := s.ethClient.EthBlockByNumber(context.Background(), big.NewInt(int64(lastSavedBlock.EthBlockNum+1))) + for lastSavedBlock.EthBlockNum < latestBlockNum { + ethBlock, err := s.ethClient.EthBlockByNumber(context.Background(), lastSavedBlock.EthBlockNum+1) if err != nil { return err } @@ -181,7 +180,7 @@ func (s *Synchronizer) reorg(uncleBlock *common.Block) error { // Iterate History DB and the blokchain looking for the latest valid block for !found && blockNum > s.firstSavedBlock.EthBlockNum { - header, err := s.ethClient.EthHeaderByNumber(context.Background(), big.NewInt(int64(blockNum))) + ethBlock, err := s.ethClient.EthBlockByNumber(context.Background(), blockNum) if err != nil { return err } @@ -190,7 +189,7 @@ func (s *Synchronizer) reorg(uncleBlock *common.Block) error { if err != nil { return err } - if block.Hash == header.Hash() { + if block.Hash == ethBlock.Hash { found = true log.Debugf("Found valid block: %v", blockNum) } else { @@ -255,12 +254,12 @@ func (s *Synchronizer) Status() (*Status, error) { // TODO: Get CurrentForgerAddr & NextForgerAddr // Check if Synchronizer is synchronized - status.Synchronized = status.CurrentBlock == latestBlockNum.Uint64() + status.Synchronized = status.CurrentBlock == latestBlockNum return status, nil } // rollupSync gets information from the Rollup Contract -func (s *Synchronizer) rollupSync(block *common.Block, lastStoredForgeL1TxsNum uint64) (*BlockData, []*BatchData, error) { +func (s *Synchronizer) rollupSync(block *common.Block, lastStoredForgeL1TxsNum int64) (*BlockData, []*BatchData, error) { // To be implemented return nil, nil, nil } diff --git a/test/ethclient.go b/test/ethclient.go index 3cfed74..b9c07c6 100644 --- a/test/ethclient.go +++ b/test/ethclient.go @@ -2,6 +2,8 @@ package test import ( "context" + "encoding/binary" + "encoding/json" "fmt" "math/big" "time" @@ -13,67 +15,273 @@ import ( "github.com/hermeznetwork/hermez-node/log" "github.com/hermeznetwork/hermez-node/utils" "github.com/iden3/go-iden3-crypto/babyjub" + "github.com/mitchellh/copystructure" ) +// RollupBlock stores all the data related to the Rollup SC from an ethereum block +type RollupBlock struct { + State eth.RollupState + Vars eth.RollupVariables + Events eth.RollupEvents +} + +// AuctionBlock stores all the data related to the Auction SC from an ethereum block +type AuctionBlock struct { + State eth.AuctionState + Vars eth.AuctionVariables + Events eth.AuctionEvents +} + +// EthereumBlock stores all the generic data related to the an ethereum block +type EthereumBlock struct { + BlockNum int64 + Time int64 + Hash ethCommon.Hash + ParentHash ethCommon.Hash + // state ethState +} + +// Block represents a ethereum block +type Block struct { + Rollup *RollupBlock + Auction *AuctionBlock + Eth *EthereumBlock +} + +// type ethState struct { +// blockNum int64 +// } + +// type state struct { +// rollupState eth.RollupState +// rollupVars eth.RollupVariables +// auctionState eth.AuctionState +// auctionVars eth.AuctionVariables +// eth ethState +// } + +// ClientSetup is used to initialize the constants of the Smart Contracts and +// other details of the test Client +type ClientSetup struct { + RollupConstants *eth.RollupConstants + RollupVariables *eth.RollupVariables + AuctionConstants *eth.AuctionConstants + AuctionVariables *eth.AuctionVariables + VerifyProof bool +} + +// Timer is an interface to simulate a source of time, useful to advance time +// virtually. +type Timer interface { + Time() int64 +} + +// type forgeBatchArgs struct { +// ethTx *types.Transaction +// blockNum int64 +// blockHash ethCommon.Hash +// } + // Client implements the eth.ClientInterface interface, allowing to manipulate the // values for testing, working with deterministic results. type Client struct { - log bool - blockNum *big.Int + log bool + rollupConstants *eth.RollupConstants + auctionConstants *eth.AuctionConstants + blocks map[int64]*Block + // state state + blockNum int64 // last mined block num + maxBlockNum int64 // highest block num calculated + timer Timer + hasher hasher + + forgeBatchArgsPending map[ethCommon.Hash]*eth.RollupForgeBatchArgs + forgeBatchArgs map[ethCommon.Hash]*eth.RollupForgeBatchArgs } // NewClient returns a new test Client that implements the eth.IClient // interface, at the given initialBlockNumber. -func NewClient(l bool, initialBlockNumber int64) *Client { +func NewClient(l bool, timer Timer, setup *ClientSetup) *Client { + blocks := make(map[int64]*Block) + blockNum := int64(0) + + hasher := hasher{} + // Add ethereum genesis block + mapL1TxQueue := make(map[int64]*eth.QueueStruct) + mapL1TxQueue[0] = eth.NewQueueStruct() + mapL1TxQueue[1] = eth.NewQueueStruct() + blockCurrent := Block{ + Rollup: &RollupBlock{ + State: eth.RollupState{ + StateRoot: big.NewInt(0), + ExitRoots: make([]*big.Int, 0), + ExitNullifierMap: make(map[[256 / 8]byte]bool), + TokenList: make([]ethCommon.Address, 0), + TokenMap: make(map[ethCommon.Address]bool), + MapL1TxQueue: mapL1TxQueue, + LastL1L2Batch: 0, + CurrentToForgeL1TxsNum: 0, + LastToForgeL1TxsNum: 1, + CurrentIdx: 0, + }, + Vars: *setup.RollupVariables, + Events: eth.NewRollupEvents(), + }, + Auction: &AuctionBlock{ + State: eth.AuctionState{ + Slots: make(map[int64]eth.SlotState), + PendingBalances: make(map[ethCommon.Address]*big.Int), + Coordinators: make(map[ethCommon.Address]eth.Coordinator), + }, + Vars: *setup.AuctionVariables, + Events: eth.NewAuctionEvents(), + }, + Eth: &EthereumBlock{ + BlockNum: blockNum, + Time: timer.Time(), + Hash: hasher.Next(), + ParentHash: ethCommon.Hash{}, + }, + } + blocks[blockNum] = &blockCurrent + blockNextRaw, err := copystructure.Copy(&blockCurrent) + if err != nil { + panic(err) + } + blockNext := blockNextRaw.(*Block) + blocks[blockNum+1] = blockNext + return &Client{ - log: l, - blockNum: big.NewInt(initialBlockNumber), + log: l, + rollupConstants: setup.RollupConstants, + auctionConstants: setup.AuctionConstants, + blocks: blocks, + timer: timer, + hasher: hasher, + forgeBatchArgsPending: make(map[ethCommon.Hash]*eth.RollupForgeBatchArgs), + forgeBatchArgs: make(map[ethCommon.Hash]*eth.RollupForgeBatchArgs), } } -// Advance moves one block forward -func (c *Client) Advance() { - c.blockNum = c.blockNum.Add(c.blockNum, big.NewInt(1)) +// +// Mock Control +// + +// Debugf calls log.Debugf if c.log is true +func (c *Client) Debugf(template string, args ...interface{}) { if c.log { - log.Debugf("TestEthClient blockNum advanced: %d", c.blockNum) + log.Debugf(template, args...) } } -// SetBlockNum sets the Client.blockNum to the given blockNum -func (c *Client) SetBlockNum(blockNum *big.Int) { - c.blockNum = blockNum +// Debugw calls log.Debugw if c.log is true +func (c *Client) Debugw(template string, kv ...interface{}) { if c.log { - log.Debugf("TestEthClient blockNum set to: %d", c.blockNum) + log.Debugw(template, kv...) } } -// EthCurrentBlock returns the current blockNum -func (c *Client) EthCurrentBlock() (*big.Int, error) { - return c.blockNum, nil +type hasher struct { + counter uint64 } -func newHeader(number *big.Int) *types.Header { - return &types.Header{ - Number: number, - Time: uint64(number.Int64()), +// Next returns the next hash +func (h *hasher) Next() ethCommon.Hash { + var hash ethCommon.Hash + binary.LittleEndian.PutUint64(hash[:], h.counter) + h.counter++ + return hash +} + +func (c *Client) nextBlock() *Block { + return c.blocks[c.blockNum+1] +} + +// CtlMineBlock moves one block forward +func (c *Client) CtlMineBlock() { + blockCurrent := c.nextBlock() + c.blockNum++ + c.maxBlockNum = c.blockNum + blockCurrent.Eth = &EthereumBlock{ + BlockNum: c.blockNum, + Time: c.timer.Time(), + Hash: c.hasher.Next(), + ParentHash: blockCurrent.Eth.Hash, + } + for ethTxHash, forgeBatchArgs := range c.forgeBatchArgsPending { + c.forgeBatchArgs[ethTxHash] = forgeBatchArgs + } + c.forgeBatchArgsPending = make(map[ethCommon.Hash]*eth.RollupForgeBatchArgs) + + blockNextRaw, err := copystructure.Copy(blockCurrent) + if err != nil { + panic(err) + } + blockNext := blockNextRaw.(*Block) + blockNext.Rollup.Events = eth.NewRollupEvents() + blockNext.Auction.Events = eth.NewAuctionEvents() + c.blocks[c.blockNum+1] = blockNext + c.Debugw("TestClient mined block", "blockNum", c.blockNum) +} + +// CtlRollback discards the last mined block. Use this to replace a mined +// block to simulate reorgs. +func (c *Client) CtlRollback() { + if c.blockNum == 0 { + panic("Can't rollback at blockNum = 0") } + delete(c.blocks, c.blockNum+1) // delete next block + delete(c.blocks, c.blockNum) // delete current block + c.blockNum-- + blockCurrent := c.blocks[c.blockNum] + blockNextRaw, err := copystructure.Copy(blockCurrent) + if err != nil { + panic(err) + } + blockNext := blockNextRaw.(*Block) + blockNext.Rollup.Events = eth.NewRollupEvents() + blockNext.Auction.Events = eth.NewAuctionEvents() + c.blocks[c.blockNum+1] = blockNext +} + +// +// Ethereum +// + +// EthCurrentBlock returns the current blockNum +func (c *Client) EthCurrentBlock() (int64, error) { + if c.blockNum < c.maxBlockNum { + panic("blockNum has decreased. " + + "After a rollback you must mine to reach the same or higher blockNum") + } + return c.blockNum, nil } +// func newHeader(number *big.Int) *types.Header { +// return &types.Header{ +// Number: number, +// Time: uint64(number.Int64()), +// } +// } + // EthHeaderByNumber returns the *types.Header for the given block number in a // deterministic way. -func (c *Client) EthHeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { - return newHeader(number), nil -} +// func (c *Client) EthHeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { +// return newHeader(number), nil +// } // EthBlockByNumber returns the *common.Block for the given block number in a // deterministic way. -func (c *Client) EthBlockByNumber(ctx context.Context, number *big.Int) (*common.Block, error) { - header := newHeader(number) - +func (c *Client) EthBlockByNumber(ctx context.Context, blockNum int64) (*common.Block, error) { + block, ok := c.blocks[blockNum] + if !ok { + return nil, fmt.Errorf("block not found") + } return &common.Block{ - EthBlockNum: uint64(number.Int64()), - Timestamp: time.Unix(number.Int64(), 0), - Hash: header.Hash(), + EthBlockNum: blockNum, + Timestamp: time.Unix(block.Eth.Time, 0), + Hash: block.Eth.Hash, + ParentHash: block.Eth.ParentHash, }, nil } @@ -83,14 +291,78 @@ var errTODO = fmt.Errorf("TODO: Not implemented yet") // Rollup // +// CtlAddL1TxUser adds an L1TxUser to the L1UserTxs queue of the Rollup +func (c *Client) CtlAddL1TxUser(l1Tx *common.L1Tx) { + nextBlock := c.nextBlock() + r := nextBlock.Rollup + queue := r.State.MapL1TxQueue[r.State.LastToForgeL1TxsNum] + if len(queue.L1TxQueue) >= c.rollupConstants.MaxL1UserTx { + r.State.LastToForgeL1TxsNum++ + r.State.MapL1TxQueue[r.State.LastToForgeL1TxsNum] = eth.NewQueueStruct() + queue = r.State.MapL1TxQueue[r.State.LastToForgeL1TxsNum] + } + if int64(l1Tx.FromIdx) > r.State.CurrentIdx { + panic("l1Tx.FromIdx > r.State.CurrentIdx") + } + if int(l1Tx.TokenID)+1 > len(r.State.TokenList) { + panic("l1Tx.TokenID + 1 > len(r.State.TokenList)") + } + queue.L1TxQueue = append(queue.L1TxQueue, *l1Tx) + r.Events.L1UserTx = append(r.Events.L1UserTx, eth.RollupEventL1UserTx{L1Tx: *l1Tx}) +} + +func (c *Client) newTransaction(name string, value interface{}) *types.Transaction { + data, err := json.Marshal(value) + if err != nil { + panic(err) + } + return types.NewTransaction(0, ethCommon.Address{}, nil, 0, nil, + data) +} + // RollupForgeBatch is the interface to call the smart contract function func (c *Client) RollupForgeBatch(*eth.RollupForgeBatchArgs) (*types.Transaction, error) { return nil, errTODO } +// CtlAddBatch adds forged batch to the Rollup, without checking any ZKProof +func (c *Client) CtlAddBatch(args *eth.RollupForgeBatchArgs) { + nextBlock := c.nextBlock() + r := nextBlock.Rollup + r.State.StateRoot = args.NewStRoot + if args.NewLastIdx < r.State.CurrentIdx { + panic("args.NewLastIdx < r.State.CurrentIdx") + } + r.State.CurrentIdx = args.NewLastIdx + r.State.ExitRoots = append(r.State.ExitRoots, args.NewExitRoot) + if args.L1Batch { + r.State.CurrentToForgeL1TxsNum++ + if r.State.CurrentToForgeL1TxsNum == r.State.LastToForgeL1TxsNum { + r.State.LastToForgeL1TxsNum++ + r.State.MapL1TxQueue[r.State.LastToForgeL1TxsNum] = eth.NewQueueStruct() + } + } + ethTx := c.newTransaction("forgebatch", args) + c.forgeBatchArgsPending[ethTx.Hash()] = args + r.Events.ForgeBatch = append(r.Events.ForgeBatch, eth.RollupEventForgeBatch{ + BatchNum: int64(len(r.State.ExitRoots)), + EthTxHash: ethTx.Hash(), + }) +} + // RollupAddToken is the interface to call the smart contract function func (c *Client) RollupAddToken(tokenAddress ethCommon.Address) (*types.Transaction, error) { - return nil, errTODO + nextBlock := c.nextBlock() + r := nextBlock.Rollup + if _, ok := r.State.TokenMap[tokenAddress]; ok { + return nil, fmt.Errorf("Token %v already registered", tokenAddress) + } + + r.State.TokenMap[tokenAddress] = true + r.State.TokenList = append(r.State.TokenList, tokenAddress) + r.Events.AddToken = append(r.Events.AddToken, eth.RollupEventAddToken{Address: tokenAddress, + TokenID: uint32(len(r.State.TokenList) - 1)}) + return c.newTransaction("addtoken", tokenAddress), nil } // RollupWithdrawSNARK is the interface to call the smart contract function @@ -185,12 +457,20 @@ func (c *Client) RollupConstants() (*eth.RollupConstants, error) { // RollupEventsByBlock returns the events in a block that happened in the Rollup Smart Contract func (c *Client) RollupEventsByBlock(blockNum int64) (*eth.RollupEvents, *ethCommon.Hash, error) { - return nil, nil, errTODO + block, ok := c.blocks[blockNum] + if !ok { + return nil, nil, fmt.Errorf("Block %v doesn't exist", blockNum) + } + return &block.Rollup.Events, &block.Eth.Hash, nil } // RollupForgeBatchArgs returns the arguments used in a ForgeBatch call in the Rollup Smart Contract in the given transaction -func (c *Client) RollupForgeBatchArgs(transaction *types.Transaction) (*eth.RollupForgeBatchArgs, error) { - return nil, errTODO +func (c *Client) RollupForgeBatchArgs(ethTxHash ethCommon.Hash) (*eth.RollupForgeBatchArgs, error) { + forgeBatchArgs, ok := c.forgeBatchArgs[ethTxHash] + if !ok { + return nil, fmt.Errorf("transaction not found") + } + return forgeBatchArgs, nil } // diff --git a/test/ethclient_test.go b/test/ethclient_test.go index 4ea0de0..112f4a8 100644 --- a/test/ethclient_test.go +++ b/test/ethclient_test.go @@ -2,45 +2,219 @@ package test import ( "context" + "crypto/ecdsa" + "encoding/binary" "math/big" "testing" "time" + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/hermeznetwork/hermez-node/common" "github.com/hermeznetwork/hermez-node/eth" + "github.com/iden3/go-iden3-crypto/babyjub" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var clientSetup *ClientSetup + +func init() { + rollupConstants := ð.RollupConstants{} + rollupVariables := ð.RollupVariables{ + MaxTxVerifiers: make([]int, 0), + TokenHEZ: ethCommon.Address{}, + GovernanceAddress: ethCommon.Address{}, + SafetyBot: ethCommon.Address{}, + ConsensusContract: ethCommon.Address{}, + WithdrawalContract: ethCommon.Address{}, + FeeAddToken: big.NewInt(1), + ForgeL1Timeout: 16, + FeeL1UserTx: big.NewInt(2), + } + auctionConstants := ð.AuctionConstants{} + auctionVariables := ð.AuctionVariables{ + DonationAddress: ethCommon.Address{}, + BootCoordinator: ethCommon.Address{}, + MinBidEpoch: [6]*big.Int{ + big.NewInt(10), big.NewInt(11), big.NewInt(12), + big.NewInt(13), big.NewInt(14), big.NewInt(15)}, + ClosedAuctionSlots: 0, + OpenAuctionSlots: 0, + AllocationRatio: [3]uint8{}, + Outbidding: 0, + SlotDeadline: 0, + } + clientSetup = &ClientSetup{ + RollupConstants: rollupConstants, + RollupVariables: rollupVariables, + AuctionConstants: auctionConstants, + AuctionVariables: auctionVariables, + } +} + +type timer struct { + time int64 +} + +func (t *timer) Time() int64 { + currentTime := t.time + t.time++ + return currentTime +} + func TestClientInterface(t *testing.T) { var c eth.ClientInterface - client := NewClient(true, 1000) + var timer timer + client := NewClient(true, &timer, clientSetup) c = client require.NotNil(t, c) } func TestEthClient(t *testing.T) { - c := NewClient(true, 1000) - - block, err := c.EthBlockByNumber(context.TODO(), big.NewInt(3)) - assert.Nil(t, err) - assert.Equal(t, uint64(3), block.EthBlockNum) - assert.Equal(t, time.Unix(3, 0), block.Timestamp) - assert.Equal(t, "0x6b0ab5a7a0ebf5f05cef3b49bc7a9739de06469a4e05557d802ee828fdf5187e", block.Hash.Hex()) - - header, err := c.EthHeaderByNumber(context.TODO(), big.NewInt(4)) - assert.Nil(t, err) - assert.Equal(t, big.NewInt(4), header.Number) - assert.Equal(t, uint64(4), header.Time) - assert.Equal(t, "0x66cdb12322040a5a345ad29cea66ca97c14d6142b53987010947c8c008e26913", header.Hash().Hex()) - - assert.Equal(t, big.NewInt(1000), c.blockNum) - c.Advance() - assert.Equal(t, big.NewInt(1001), c.blockNum) - c.Advance() - assert.Equal(t, big.NewInt(1002), c.blockNum) - - c.SetBlockNum(big.NewInt(5000)) - assert.Equal(t, big.NewInt(5000), c.blockNum) - c.Advance() - assert.Equal(t, big.NewInt(5001), c.blockNum) + token1Addr := ethCommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f") + + var timer timer + c := NewClient(true, &timer, clientSetup) + blockNum, err := c.EthCurrentBlock() + require.Nil(t, err) + assert.Equal(t, int64(0), blockNum) + + block, err := c.EthBlockByNumber(context.TODO(), 0) + require.Nil(t, err) + assert.Equal(t, int64(0), block.EthBlockNum) + assert.Equal(t, time.Unix(0, 0), block.Timestamp) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", block.Hash.Hex()) + assert.Equal(t, int64(0), c.blockNum) + + // Mine some empty blocks + + c.CtlMineBlock() + assert.Equal(t, int64(1), c.blockNum) + c.CtlMineBlock() + assert.Equal(t, int64(2), c.blockNum) + + block, err = c.EthBlockByNumber(context.TODO(), 2) + require.Nil(t, err) + assert.Equal(t, int64(2), block.EthBlockNum) + assert.Equal(t, time.Unix(2, 0), block.Timestamp) + + // Add a token + + tx, err := c.RollupAddToken(token1Addr) + require.Nil(t, err) + assert.NotNil(t, tx) + + // Add some L1UserTxs + // Create Accounts + + const N = 16 + var keys [N]*keys + for i := 0; i < N; i++ { + keys[i] = genKeys(int64(i)) + l1UserTx := common.L1Tx{ + FromIdx: common.Idx(0), + FromEthAddr: keys[i].Addr, + FromBJJ: keys[i].BJJPublicKey, + TokenID: common.TokenID(0), + LoadAmount: big.NewInt(10 + int64(i)), + } + c.CtlAddL1TxUser(&l1UserTx) + } + c.CtlMineBlock() + + blockNum, err = c.EthCurrentBlock() + require.Nil(t, err) + rollupEvents, _, err := c.RollupEventsByBlock(blockNum) + require.Nil(t, err) + assert.Equal(t, N, len(rollupEvents.L1UserTx)) + assert.Equal(t, 1, len(rollupEvents.AddToken)) + + // Forge a batch + + c.CtlAddBatch(ð.RollupForgeBatchArgs{ + NewLastIdx: 0, + NewStRoot: big.NewInt(1), + NewExitRoot: big.NewInt(100), + L1CoordinatorTxs: []*common.L1Tx{}, + L2Txs: []*common.L2Tx{}, + FeeIdxCoordinator: make([]common.Idx, eth.FeeIdxCoordinatorLen), + VerifierIdx: 0, + L1Batch: true, + }) + c.CtlMineBlock() + + blockNumA, err := c.EthCurrentBlock() + require.Nil(t, err) + rollupEvents, hashA, err := c.RollupEventsByBlock(blockNumA) + require.Nil(t, err) + assert.Equal(t, 0, len(rollupEvents.L1UserTx)) + assert.Equal(t, 0, len(rollupEvents.AddToken)) + assert.Equal(t, 1, len(rollupEvents.ForgeBatch)) + + // Simulate reorg discarding last mined block + + c.CtlRollback() + c.CtlMineBlock() + + blockNumB, err := c.EthCurrentBlock() + require.Nil(t, err) + rollupEvents, hashB, err := c.RollupEventsByBlock(blockNumA) + require.Nil(t, err) + assert.Equal(t, 0, len(rollupEvents.L1UserTx)) + assert.Equal(t, 0, len(rollupEvents.AddToken)) + assert.Equal(t, 0, len(rollupEvents.ForgeBatch)) + + assert.Equal(t, blockNumA, blockNumB) + assert.NotEqual(t, hashA, hashB) + + // Forge again + rollupForgeBatchArgs0 := ð.RollupForgeBatchArgs{ + NewLastIdx: 0, + NewStRoot: big.NewInt(1), + NewExitRoot: big.NewInt(100), + L1CoordinatorTxs: []*common.L1Tx{}, + L2Txs: []*common.L2Tx{}, + FeeIdxCoordinator: make([]common.Idx, eth.FeeIdxCoordinatorLen), + VerifierIdx: 0, + L1Batch: true, + } + c.CtlAddBatch(rollupForgeBatchArgs0) + c.CtlMineBlock() + + // Retrieve ForgeBatchArguments starting from the events + + blockNum, err = c.EthCurrentBlock() + require.Nil(t, err) + rollupEvents, _, err = c.RollupEventsByBlock(blockNum) + require.Nil(t, err) + + rollupForgeBatchArgs1, err := c.RollupForgeBatchArgs(rollupEvents.ForgeBatch[0].EthTxHash) + require.Nil(t, err) + assert.Equal(t, rollupForgeBatchArgs0, rollupForgeBatchArgs1) +} + +type keys struct { + BJJSecretKey *babyjub.PrivateKey + BJJPublicKey *babyjub.PublicKey + Addr ethCommon.Address +} + +func genKeys(i int64) *keys { + i++ // i = 0 doesn't work for the ecdsa key generation + var sk babyjub.PrivateKey + binary.LittleEndian.PutUint64(sk[:], uint64(i)) + + // eth address + var key ecdsa.PrivateKey + key.D = big.NewInt(i) // only for testing + key.PublicKey.X, key.PublicKey.Y = ethCrypto.S256().ScalarBaseMult(key.D.Bytes()) + key.Curve = ethCrypto.S256() + addr := ethCrypto.PubkeyToAddress(key.PublicKey) + + return &keys{ + BJJSecretKey: &sk, + BJJPublicKey: sk.Public(), + Addr: addr, + } }