package synchronizer import ( "context" "encoding/json" "fmt" "io/ioutil" "math/big" "os" "sort" "testing" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/hermeznetwork/hermez-node/common" dbUtils "github.com/hermeznetwork/hermez-node/db" "github.com/hermeznetwork/hermez-node/db/historydb" "github.com/hermeznetwork/hermez-node/db/statedb" "github.com/hermeznetwork/hermez-node/eth" "github.com/hermeznetwork/hermez-node/test" "github.com/hermeznetwork/hermez-node/test/til" "github.com/jinzhu/copier" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var tokenConsts = map[common.TokenID]eth.ERC20Consts{} type timer struct { time int64 } func (t *timer) Time() int64 { currentTime := t.time t.time++ return currentTime } func accountsCmp(accounts []common.Account) func(i, j int) bool { return func(i, j int) bool { return accounts[i].Idx < accounts[j].Idx } } // Check Sync output and HistoryDB state against expected values generated by // til func checkSyncBlock(t *testing.T, s *Synchronizer, blockNum int, block, syncBlock *common.BlockData) { // Check Blocks dbBlocks, err := s.historyDB.GetAllBlocks() require.Nil(t, err) dbBlocks = dbBlocks[1:] // ignore block 0, added by default in the DB assert.Equal(t, blockNum, len(dbBlocks)) assert.Equal(t, int64(blockNum), dbBlocks[blockNum-1].EthBlockNum) assert.NotEqual(t, dbBlocks[blockNum-1].Hash, dbBlocks[blockNum-2].Hash) assert.Greater(t, dbBlocks[blockNum-1].Timestamp.Unix(), dbBlocks[blockNum-2].Timestamp.Unix()) // Check Tokens assert.Equal(t, len(block.Rollup.AddedTokens), len(syncBlock.Rollup.AddedTokens)) dbTokens, err := s.historyDB.GetAllTokens() require.Nil(t, err) dbTokens = dbTokens[1:] // ignore token 0, added by default in the DB for i, token := range block.Rollup.AddedTokens { dbToken := dbTokens[i] syncToken := syncBlock.Rollup.AddedTokens[i] assert.Equal(t, block.Block.EthBlockNum, syncToken.EthBlockNum) assert.Equal(t, token.TokenID, syncToken.TokenID) assert.Equal(t, token.EthAddr, syncToken.EthAddr) tokenConst := tokenConsts[token.TokenID] assert.Equal(t, tokenConst.Name, syncToken.Name) assert.Equal(t, tokenConst.Symbol, syncToken.Symbol) assert.Equal(t, tokenConst.Decimals, syncToken.Decimals) var tokenCpy historydb.TokenWithUSD //nolint:gosec require.Nil(t, copier.Copy(&tokenCpy, &token)) // copy common.Token to historydb.TokenWithUSD require.Nil(t, copier.Copy(&tokenCpy, &tokenConst)) // copy common.Token to historydb.TokenWithUSD tokenCpy.ItemID = dbToken.ItemID // we don't care about ItemID assert.Equal(t, tokenCpy, dbToken) } // Check L1UserTxs assert.Equal(t, len(block.Rollup.L1UserTxs), len(syncBlock.Rollup.L1UserTxs)) dbL1UserTxs, err := s.historyDB.GetAllL1UserTxs() require.Nil(t, err) // Ignore BatchNum in syncBlock.L1UserTxs because this value is set by the HistoryDB for i := range syncBlock.Rollup.L1UserTxs { syncBlock.Rollup.L1UserTxs[i].BatchNum = block.Rollup.L1UserTxs[i].BatchNum } assert.Equal(t, block.Rollup.L1UserTxs, syncBlock.Rollup.L1UserTxs) for _, tx := range block.Rollup.L1UserTxs { var dbTx *common.L1Tx // Find tx in DB output for _, _dbTx := range dbL1UserTxs { if *tx.ToForgeL1TxsNum == *_dbTx.ToForgeL1TxsNum && tx.Position == _dbTx.Position { dbTx = new(common.L1Tx) *dbTx = _dbTx break } } assert.Equal(t, &tx, dbTx) //nolint:gosec } // Check Batches assert.Equal(t, len(block.Rollup.Batches), len(syncBlock.Rollup.Batches)) dbBatches, err := s.historyDB.GetAllBatches() require.Nil(t, err) dbL1CoordinatorTxs, err := s.historyDB.GetAllL1CoordinatorTxs() require.Nil(t, err) dbL2Txs, err := s.historyDB.GetAllL2Txs() require.Nil(t, err) dbExits, err := s.historyDB.GetAllExits() require.Nil(t, err) // dbL1CoordinatorTxs := []common.L1Tx{} for i, batch := range block.Rollup.Batches { var dbBatch *common.Batch // Find batch in DB output for _, _dbBatch := range dbBatches { if batch.Batch.BatchNum == _dbBatch.BatchNum { dbBatch = new(common.Batch) *dbBatch = _dbBatch break } } syncBatch := syncBlock.Rollup.Batches[i] // We don't care about TotalFeesUSD. Use the syncBatch that // has a TotalFeesUSD inserted by the HistoryDB batch.Batch.TotalFeesUSD = syncBatch.Batch.TotalFeesUSD assert.Equal(t, batch.CreatedAccounts, syncBatch.CreatedAccounts) batch.Batch.NumAccounts = len(batch.CreatedAccounts) // Test field by field to facilitate debugging of errors assert.Equal(t, batch.L1CoordinatorTxs, syncBatch.L1CoordinatorTxs) assert.Equal(t, batch.L2Txs, syncBatch.L2Txs) // In exit tree, we only check AccountIdx and Balance, because // it's what we have precomputed before. for j := range batch.ExitTree { exit := &batch.ExitTree[j] assert.Equal(t, exit.AccountIdx, syncBatch.ExitTree[j].AccountIdx) assert.Equal(t, exit.Balance, syncBatch.ExitTree[j].Balance) *exit = syncBatch.ExitTree[j] } assert.Equal(t, batch.Batch, syncBatch.Batch) assert.Equal(t, batch, syncBatch) assert.Equal(t, &batch.Batch, dbBatch) //nolint:gosec // Check L1CoordinatorTxs from DB for _, tx := range batch.L1CoordinatorTxs { var dbTx *common.L1Tx // Find tx in DB output for _, _dbTx := range dbL1CoordinatorTxs { if *tx.BatchNum == *_dbTx.BatchNum && tx.Position == _dbTx.Position { dbTx = new(common.L1Tx) *dbTx = _dbTx break } } assert.Equal(t, &tx, dbTx) //nolint:gosec } // Check L2Txs from DB for _, tx := range batch.L2Txs { var dbTx *common.L2Tx // Find tx in DB output for _, _dbTx := range dbL2Txs { if tx.BatchNum == _dbTx.BatchNum && tx.Position == _dbTx.Position { dbTx = new(common.L2Tx) *dbTx = _dbTx break } } assert.Equal(t, &tx, dbTx) //nolint:gosec } // Check Exits from DB for _, exit := range batch.ExitTree { var dbExit *common.ExitInfo // Find exit in DB output for _, _dbExit := range dbExits { if exit.BatchNum == _dbExit.BatchNum && exit.AccountIdx == _dbExit.AccountIdx { dbExit = new(common.ExitInfo) *dbExit = _dbExit break } } // Compare MerkleProof in JSON because unmarshaled 0 // big.Int leaves the internal big.Int array at nil, // and gives trouble when comparing big.Int with // internal big.Int array != nil but empty. mtp, err := json.Marshal(exit.MerkleProof) require.Nil(t, err) dbMtp, err := json.Marshal(dbExit.MerkleProof) require.Nil(t, err) assert.Equal(t, mtp, dbMtp) dbExit.MerkleProof = exit.MerkleProof assert.Equal(t, &exit, dbExit) //nolint:gosec } } // Compare accounts from HistoryDB with StateDB (they should match) dbAccounts, err := s.historyDB.GetAllAccounts() require.Nil(t, err) sdbAccounts, err := s.stateDB.GetAccounts() require.Nil(t, err) assertEqualAccountsHistoryDBStateDB(t, dbAccounts, sdbAccounts) } func assertEqualAccountsHistoryDBStateDB(t *testing.T, hdbAccs, sdbAccs []common.Account) { assert.Equal(t, len(hdbAccs), len(sdbAccs)) sort.SliceStable(hdbAccs, accountsCmp(hdbAccs)) sort.SliceStable(sdbAccs, accountsCmp(sdbAccs)) for i := range hdbAccs { hdbAcc := hdbAccs[i] sdbAcc := sdbAccs[i] assert.Equal(t, hdbAcc.Idx, sdbAcc.Idx) assert.Equal(t, hdbAcc.TokenID, sdbAcc.TokenID) assert.Equal(t, hdbAcc.EthAddr, sdbAcc.EthAddr) assert.Equal(t, hdbAcc.PublicKey, sdbAcc.PublicKey) } } // ethAddTokens adds the tokens from the blocks to the blockchain func ethAddTokens(blocks []common.BlockData, client *test.Client) { for _, block := range blocks { for _, token := range block.Rollup.AddedTokens { consts := eth.ERC20Consts{ Name: fmt.Sprintf("Token %d", token.TokenID), Symbol: fmt.Sprintf("TK%d", token.TokenID), Decimals: 18, } tokenConsts[token.TokenID] = consts client.CtlAddERC20(token.EthAddr, consts) } } } // ethAddBlocks adds block data to the smart contracts func ethAddBlocks(t *testing.T, blocks []common.BlockData, client *test.Client, clientSetup *test.ClientSetup) { for _, block := range blocks { for _, token := range block.Rollup.AddedTokens { _, err := client.RollupAddTokenSimple(token.EthAddr, clientSetup.RollupVariables.FeeAddToken) require.Nil(t, err) } for _, tx := range block.Rollup.L1UserTxs { client.CtlSetAddr(tx.FromEthAddr) _, err := client.RollupL1UserTxERC20ETH(tx.FromBJJ, int64(tx.FromIdx), tx.LoadAmount, tx.Amount, uint32(tx.TokenID), int64(tx.ToIdx)) require.Nil(t, err) } client.CtlSetAddr(clientSetup.AuctionVariables.BootCoordinator) for _, batch := range block.Rollup.Batches { _, err := client.RollupForgeBatch(ð.RollupForgeBatchArgs{ NewLastIdx: batch.Batch.LastIdx, NewStRoot: batch.Batch.StateRoot, NewExitRoot: batch.Batch.ExitRoot, L1CoordinatorTxs: batch.L1CoordinatorTxs, L1CoordinatorTxsAuths: [][]byte{}, // Intentionally empty L2TxsData: batch.L2Txs, FeeIdxCoordinator: batch.Batch.FeeIdxsCoordinator, // Circuit selector VerifierIdx: 0, // Intentionally empty L1Batch: batch.L1Batch, ProofA: [2]*big.Int{}, // Intentionally empty ProofB: [2][2]*big.Int{}, // Intentionally empty ProofC: [2]*big.Int{}, // Intentionally empty }) require.Nil(t, err) } // Mine block and sync client.CtlMineBlock() } } func TestSync(t *testing.T) { // // Setup // ctx := context.Background() // Int State DB dir, err := ioutil.TempDir("", "tmpdb") require.Nil(t, err) defer assert.Nil(t, os.RemoveAll(dir)) stateDB, err := statedb.NewStateDB(dir, statedb.TypeSynchronizer, 32) assert.Nil(t, err) // Init History DB pass := os.Getenv("POSTGRES_PASS") db, err := dbUtils.InitSQLDB(5432, "localhost", "hermez", pass, "hermez") require.Nil(t, err) historyDB := historydb.NewHistoryDB(db) // Clear DB test.WipeDB(historyDB.DB()) // Init eth client var timer timer clientSetup := test.NewClientSetupExample() bootCoordAddr := clientSetup.AuctionVariables.BootCoordinator client := test.NewClient(true, &timer, ðCommon.Address{}, clientSetup) // Create Synchronizer s, err := NewSynchronizer(client, historyDB, stateDB, Config{ StartBlockNum: ConfigStartBlockNum{ Rollup: 1, Auction: 1, WDelayer: 1, }, InitialVariables: SCVariables{ Rollup: *clientSetup.RollupVariables, Auction: *clientSetup.AuctionVariables, WDelayer: *clientSetup.WDelayerVariables, }, }) require.Nil(t, err) // // First Sync from an initial state // // Test Sync for rollup genesis block syncBlock, discards, err := s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.NotNil(t, syncBlock) assert.Equal(t, int64(1), syncBlock.Block.EthBlockNum) dbBlocks, err := s.historyDB.GetAllBlocks() require.Nil(t, err) assert.Equal(t, 2, len(dbBlocks)) assert.Equal(t, int64(1), dbBlocks[1].EthBlockNum) // Sync again and expect no new blocks syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.Nil(t, syncBlock) // // Generate blockchain and smart contract data, and fill the test smart contracts // // Generate blockchain data with til set1 := ` Type: Blockchain AddToken(1) AddToken(2) AddToken(3) CreateAccountDeposit(1) C: 2000 // Idx=256+2=258 CreateAccountDeposit(2) A: 2000 // Idx=256+3=259 CreateAccountDeposit(1) D: 500 // Idx=256+4=260 CreateAccountDeposit(2) B: 500 // Idx=256+5=261 CreateAccountDeposit(2) C: 500 // Idx=256+6=262 CreateAccountCoordinator(1) A // Idx=256+0=256 CreateAccountCoordinator(1) B // Idx=256+1=257 > batchL1 // forge L1UserTxs{nil}, freeze defined L1UserTxs{5} > batchL1 // forge defined L1UserTxs{5}, freeze L1UserTxs{nil} > block // blockNum=2 CreateAccountDepositTransfer(1) E-A: 1000, 200 // Idx=256+7=263 ForceExit(1) A: 100 ForceExit(1) B: 80 ForceTransfer(1) A-D: 100 Transfer(1) C-A: 100 (200) Exit(1) C: 50 (200) Exit(1) D: 30 (200) > batchL1 // forge L1UserTxs{nil}, freeze defined L1UserTxs{3} > batchL1 // forge L1UserTxs{3}, freeze defined L1UserTxs{nil} > block // blockNum=3 ` tc := til.NewContext(common.RollupConstMaxL1UserTx) tilCfgExtra := til.ConfigExtra{ BootCoordAddr: bootCoordAddr, CoordUser: "A", } blocks, err := tc.GenerateBlocks(set1) require.Nil(t, err) // Sanity check require.Equal(t, 2, len(blocks)) // blocks 0 (blockNum=2) i := 0 require.Equal(t, 2, int(blocks[i].Block.EthBlockNum)) require.Equal(t, 3, len(blocks[i].Rollup.AddedTokens)) require.Equal(t, 5, len(blocks[i].Rollup.L1UserTxs)) require.Equal(t, 2, len(blocks[i].Rollup.Batches)) require.Equal(t, 2, len(blocks[i].Rollup.Batches[0].L1CoordinatorTxs)) // blocks 1 (blockNum=3) i = 1 require.Equal(t, 3, int(blocks[i].Block.EthBlockNum)) require.Equal(t, 4, len(blocks[i].Rollup.L1UserTxs)) require.Equal(t, 2, len(blocks[i].Rollup.Batches)) require.Equal(t, 3, len(blocks[i].Rollup.Batches[0].L2Txs)) // Generate extra required data ethAddTokens(blocks, client) err = tc.FillBlocksExtra(blocks, &tilCfgExtra) assert.Nil(t, err) tc.FillBlocksL1UserTxsBatchNum(blocks) // Add block data to the smart contracts ethAddBlocks(t, blocks, client, clientSetup) // // Sync to synchronize the current state from the test smart contracts, // and check the outcome // // Block 2 syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.NotNil(t, syncBlock) assert.Equal(t, int64(2), syncBlock.Block.EthBlockNum) checkSyncBlock(t, s, 2, &blocks[0], syncBlock) // Block 3 syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.NotNil(t, syncBlock) assert.Equal(t, int64(3), syncBlock.Block.EthBlockNum) checkSyncBlock(t, s, 3, &blocks[1], syncBlock) // Block 4 // Generate 2 withdraws manually _, err = client.RollupWithdrawMerkleProof(tc.Users["A"].BJJ.Public(), 1, 4, 256, big.NewInt(100), []*big.Int{}, true) require.Nil(t, err) _, err = client.RollupWithdrawMerkleProof(tc.Users["C"].BJJ.Public(), 1, 3, 258, big.NewInt(50), []*big.Int{}, false) require.Nil(t, err) client.CtlMineBlock() syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.NotNil(t, syncBlock) assert.Equal(t, int64(4), syncBlock.Block.EthBlockNum) dbExits, err := s.historyDB.GetAllExits() require.Nil(t, err) foundA1, foundC1 := false, false for _, exit := range dbExits { if exit.AccountIdx == 256 && exit.BatchNum == 4 { foundA1 = true assert.Equal(t, int64(4), *exit.InstantWithdrawn) } if exit.AccountIdx == 258 && exit.BatchNum == 3 { foundC1 = true assert.Equal(t, int64(4), *exit.DelayedWithdrawRequest) } } assert.True(t, foundA1) assert.True(t, foundC1) // Block 5 // Update variables manually rollupVars, auctionVars, wDelayerVars, err := s.historyDB.GetSCVars() require.Nil(t, err) rollupVars.ForgeL1L2BatchTimeout = 42 _, err = client.RollupUpdateForgeL1L2BatchTimeout(rollupVars.ForgeL1L2BatchTimeout) require.Nil(t, err) auctionVars.OpenAuctionSlots = 17 _, err = client.AuctionSetOpenAuctionSlots(auctionVars.OpenAuctionSlots) require.Nil(t, err) wDelayerVars.WithdrawalDelay = 99 _, err = client.WDelayerChangeWithdrawalDelay(wDelayerVars.WithdrawalDelay) require.Nil(t, err) client.CtlMineBlock() syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.NotNil(t, syncBlock) assert.Equal(t, int64(5), syncBlock.Block.EthBlockNum) dbRollupVars, dbAuctionVars, dbWDelayerVars, err := s.historyDB.GetSCVars() require.Nil(t, err) // Set EthBlockNum for Vars to the blockNum in which they were updated (should be 5) rollupVars.EthBlockNum = syncBlock.Block.EthBlockNum auctionVars.EthBlockNum = syncBlock.Block.EthBlockNum wDelayerVars.EthBlockNum = syncBlock.Block.EthBlockNum assert.Equal(t, rollupVars, dbRollupVars) assert.Equal(t, auctionVars, dbAuctionVars) assert.Equal(t, wDelayerVars, dbWDelayerVars) // // Reorg test // // Redo blocks 2-5 (as a reorg) only leaving: // - 2 create account transactions // - 2 add tokens // We add a 6th block so that the synchronizer can detect the reorg set2 := ` Type: Blockchain AddToken(1) AddToken(2) CreateAccountDeposit(1) C: 2000 // Idx=256+1=257 CreateAccountCoordinator(1) A // Idx=256+0=256 > batchL1 // forge L1UserTxs{nil}, freeze defined L1UserTxs{1} > batchL1 // forge defined L1UserTxs{1}, freeze L1UserTxs{nil} > block // blockNum=2 > block // blockNum=3 > block // blockNum=4 > block // blockNum=5 > block // blockNum=6 ` tc = til.NewContext(common.RollupConstMaxL1UserTx) tilCfgExtra = til.ConfigExtra{ BootCoordAddr: bootCoordAddr, CoordUser: "A", } blocks, err = tc.GenerateBlocks(set2) require.Nil(t, err) for i := 0; i < 4; i++ { client.CtlRollback() } blockNum := client.CtlLastBlock() require.Equal(t, int64(1), blockNum) // Generate extra required data ethAddTokens(blocks, client) err = tc.FillBlocksExtra(blocks, &tilCfgExtra) assert.Nil(t, err) tc.FillBlocksL1UserTxsBatchNum(blocks) // Add block data to the smart contracts ethAddBlocks(t, blocks, client, clientSetup) // First sync detects the reorg and discards 4 blocks syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) expetedDiscards := int64(4) require.Equal(t, &expetedDiscards, discards) require.Nil(t, syncBlock) // At this point, the DB only has data up to block 1 dbBlock, err := s.historyDB.GetLastBlock() require.Nil(t, err) assert.Equal(t, int64(1), dbBlock.EthBlockNum) // Accounts in HistoryDB and StateDB must be empty dbAccounts, err := s.historyDB.GetAllAccounts() require.Nil(t, err) sdbAccounts, err := s.stateDB.GetAccounts() require.Nil(t, err) assert.Equal(t, 0, len(dbAccounts)) assertEqualAccountsHistoryDBStateDB(t, dbAccounts, sdbAccounts) // Sync blocks 2-6 for i := 0; i < 5; i++ { syncBlock, discards, err = s.Sync2(ctx, nil) require.Nil(t, err) require.Nil(t, discards) require.NotNil(t, syncBlock) assert.Equal(t, int64(2+i), syncBlock.Block.EthBlockNum) } dbBlock, err = s.historyDB.GetLastBlock() require.Nil(t, err) assert.Equal(t, int64(6), dbBlock.EthBlockNum) // Accounts in HistoryDB and StateDB is only 2 entries dbAccounts, err = s.historyDB.GetAllAccounts() require.Nil(t, err) sdbAccounts, err = s.stateDB.GetAccounts() require.Nil(t, err) assert.Equal(t, 2, len(dbAccounts)) assertEqualAccountsHistoryDBStateDB(t, dbAccounts, sdbAccounts) }