diff --git a/core/blockchain.go b/core/blockchain.go index 3cfa21654f..7157d39455 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -974,6 +974,42 @@ func (bc *BlockChain) GetDiffLayerRLP(blockHash common.Hash) rlp.RawValue { return rawData } +func (bc *BlockChain) GetDiffAccounts(blockHash common.Hash) ([]common.Address, error) { + var ( + accounts []common.Address + diffLayer *types.DiffLayer + ) + + header := bc.GetHeaderByHash(blockHash) + if header == nil { + return nil, fmt.Errorf("no block found") + } + + if cached, ok := bc.diffLayerCache.Get(blockHash); ok { + diffLayer = cached.(*types.DiffLayer) + } else if diffStore := bc.db.DiffStore(); diffStore != nil { + diffLayer = rawdb.ReadDiffLayer(diffStore, blockHash) + } + + if diffLayer == nil { + if header.TxHash != types.EmptyRootHash { + return nil, fmt.Errorf("no diff layer found") + } + + return nil, nil + } + + for _, diffAccounts := range diffLayer.Accounts { + accounts = append(accounts, diffAccounts.Account) + } + + if header.TxHash != types.EmptyRootHash && len(accounts) == 0 { + return nil, fmt.Errorf("no diff account in block, maybe bad diff layer") + } + + return accounts, nil +} + // HasBlock checks if a block is fully present in the database or not. func (bc *BlockChain) HasBlock(hash common.Hash, number uint64) bool { if bc.blockCache.Contains(hash) { diff --git a/core/blockchain_diff_test.go b/core/blockchain_diff_test.go index 717b4039ed..bec463136e 100644 --- a/core/blockchain_diff_test.go +++ b/core/blockchain_diff_test.go @@ -45,8 +45,79 @@ var ( testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") // testAddr is the Ethereum address of the tester account. testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + // testBlocks is the test parameters array for specific blocks. + testBlocks = []testBlockParam{ + { + // This txs params also used to default block. + blockNr: 11, + txs: []testTransactionParam{ + { + to: common.Address{0x01}, + value: big.NewInt(1), + gasPrice: big.NewInt(1), + data: nil, + }, + }, + }, + { + blockNr: 12, + txs: []testTransactionParam{ + { + to: common.Address{0x01}, + value: big.NewInt(1), + gasPrice: big.NewInt(1), + data: nil, + }, + { + to: common.Address{0x02}, + value: big.NewInt(2), + gasPrice: big.NewInt(2), + data: nil, + }, + }, + }, + { + blockNr: 13, + txs: []testTransactionParam{ + { + to: common.Address{0x01}, + value: big.NewInt(1), + gasPrice: big.NewInt(1), + data: nil, + }, + { + to: common.Address{0x02}, + value: big.NewInt(2), + gasPrice: big.NewInt(2), + data: nil, + }, + { + to: common.Address{0x03}, + value: big.NewInt(3), + gasPrice: big.NewInt(3), + data: nil, + }, + }, + }, + { + blockNr: 14, + txs: []testTransactionParam{}, + }, + } ) +type testTransactionParam struct { + to common.Address + value *big.Int + gasPrice *big.Int + data []byte +} + +type testBlockParam struct { + blockNr int + txs []testTransactionParam +} + // testBackend is a mock implementation of the live Ethereum message handler. Its // purpose is to allow testing the request/reply workflows and wire serialization // in the `eth` protocol without actually doing any data processing. @@ -78,13 +149,35 @@ func newTestBackendWithGenerator(blocks int, lightProcess bool) *testBackend { // lets unset (nil). Set it here to the correct value. block.SetCoinbase(testAddr) - // We want to simulate an empty middle block, having the same state as the - // first one. The last is needs a state change again to force a reorg. - tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), common.Address{0x01}, big.NewInt(1), params.TxGas, big.NewInt(1), nil), signer, testKey) - if err != nil { - panic(err) + for idx, testBlock := range testBlocks { + // Specific block setting, the index in this generator has 1 diff from specified blockNr. + if i+1 == testBlock.blockNr { + for _, testTransaction := range testBlock.txs { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), testTransaction.to, + testTransaction.value, params.TxGas, testTransaction.gasPrice, testTransaction.data), signer, testKey) + if err != nil { + panic(err) + } + block.AddTxWithChain(chain, tx) + } + break + } + + // Default block setting. + if idx == len(testBlocks)-1 { + // We want to simulate an empty middle block, having the same state as the + // first one. The last is needs a state change again to force a reorg. + for _, testTransaction := range testBlocks[0].txs { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), testTransaction.to, + testTransaction.value, params.TxGas, testTransaction.gasPrice, testTransaction.data), signer, testKey) + if err != nil { + panic(err) + } + block.AddTxWithChain(chain, tx) + } + } } - block.AddTxWithChain(chain, tx) + } bs, _ := GenerateChain(params.TestChainConfig, chain.Genesis(), ethash.NewFaker(), db, blocks, generator) if _, err := chain.InsertChain(bs); err != nil { @@ -139,12 +232,17 @@ func TestProcessDiffLayer(t *testing.T) { } blockHash := block.Hash() rawDiff := fullBackend.chain.GetDiffLayerRLP(blockHash) - diff, err := rawDataToDiffLayer(rawDiff) - if err != nil { - t.Errorf("failed to decode rawdata %v", err) + if len(rawDiff) != 0 { + diff, err := rawDataToDiffLayer(rawDiff) + if err != nil { + t.Errorf("failed to decode rawdata %v", err) + } + if diff == nil { + continue + } + lightBackend.Chain().HandleDiffLayer(diff, "testpid", true) } - lightBackend.Chain().HandleDiffLayer(diff, "testpid", true) - _, err = lightBackend.chain.insertChain([]*types.Block{block}, true) + _, err := lightBackend.chain.insertChain([]*types.Block{block}, true) if err != nil { t.Errorf("failed to insert block %v", err) } @@ -186,7 +284,8 @@ func TestFreezeDiffLayer(t *testing.T) { blockNum := 1024 fullBackend := newTestBackend(blockNum, true) defer fullBackend.close() - if fullBackend.chain.diffQueue.Size() != blockNum { + // Minus one empty block. + if fullBackend.chain.diffQueue.Size() != blockNum-1 { t.Errorf("size of diff queue is wrong, expected: %d, get: %d", blockNum, fullBackend.chain.diffQueue.Size()) } time.Sleep(diffLayerFreezerRecheckInterval + 1*time.Second) @@ -215,10 +314,11 @@ func TestPruneDiffLayer(t *testing.T) { for num := uint64(1); num < uint64(blockNum); num++ { header := fullBackend.chain.GetHeaderByNumber(num) rawDiff := fullBackend.chain.GetDiffLayerRLP(header.Hash()) - diff, _ := rawDataToDiffLayer(rawDiff) - fullBackend.Chain().HandleDiffLayer(diff, "testpid1", true) - fullBackend.Chain().HandleDiffLayer(diff, "testpid2", true) - + if len(rawDiff) != 0 { + diff, _ := rawDataToDiffLayer(rawDiff) + fullBackend.Chain().HandleDiffLayer(diff, "testpid1", true) + fullBackend.Chain().HandleDiffLayer(diff, "testpid2", true) + } } fullBackend.chain.pruneDiffLayer() if len(fullBackend.chain.diffNumToBlockHashes) != maxDiffForkDist { @@ -261,3 +361,45 @@ func TestPruneDiffLayer(t *testing.T) { } } + +func TestGetDiffAccounts(t *testing.T) { + t.Parallel() + + blockNum := 128 + fullBackend := newTestBackend(blockNum, false) + defer fullBackend.close() + + for _, testBlock := range testBlocks { + block := fullBackend.chain.GetBlockByNumber(uint64(testBlock.blockNr)) + if block == nil { + t.Fatal("block should not be nil") + } + blockHash := block.Hash() + accounts, err := fullBackend.chain.GetDiffAccounts(blockHash) + if err != nil { + t.Errorf("get diff accounts eror for block number (%d): %v", testBlock.blockNr, err) + } + + for idx, account := range accounts { + if testAddr == account { + break + } + + if idx == len(accounts)-1 { + t.Errorf("the diff accounts does't include addr: %v", testAddr) + } + } + + for _, transaction := range testBlock.txs { + for idx, account := range accounts { + if transaction.to == account { + break + } + + if idx == len(accounts)-1 { + t.Errorf("the diff accounts does't include addr: %v", transaction.to) + } + } + } + } +} diff --git a/core/types/block.go b/core/types/block.go index a577e60516..bee5d80cdd 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -453,3 +453,14 @@ type DiffStorage struct { Keys []string Vals [][]byte } + +type DiffAccountsInTx struct { + TxHash common.Hash + Accounts map[common.Address]*big.Int +} + +type DiffAccountsInBlock struct { + Number uint64 + BlockHash common.Hash + Transactions []DiffAccountsInTx +} diff --git a/eth/api_backend.go b/eth/api_backend.go index 7ac1f82a86..5c864a236b 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -279,6 +279,10 @@ func (b *EthAPIBackend) SuggestPrice(ctx context.Context) (*big.Int, error) { return b.gpo.SuggestPrice(ctx) } +func (b *EthAPIBackend) Chain() *core.BlockChain { + return b.eth.BlockChain() +} + func (b *EthAPIBackend) ChainDb() ethdb.Database { return b.eth.ChainDb() } diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 81fbe5b407..578b10f09a 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -186,6 +186,20 @@ func (ec *Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.H return head, err } +// GetDiffAccounts returns changed accounts in a specific block number. +func (ec *Client) GetDiffAccounts(ctx context.Context, number *big.Int) ([]common.Address, error) { + accounts := make([]common.Address, 0) + err := ec.c.CallContext(ctx, &accounts, "eth_getDiffAccounts", toBlockNumArg(number)) + return accounts, err +} + +// GetDiffAccountsWithScope returns detailed changes of some interested accounts in a specific block number. +func (ec *Client) GetDiffAccountsWithScope(ctx context.Context, number *big.Int, accounts []common.Address) (*types.DiffAccountsInBlock, error) { + var result types.DiffAccountsInBlock + err := ec.c.CallContext(ctx, &result, "eth_getDiffAccountsWithScope", toBlockNumArg(number), accounts) + return &result, err +} + type rpcTransaction struct { tx *types.Transaction txExtraInfo diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 341acf978f..d2fd056041 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -31,9 +31,11 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" @@ -181,11 +183,82 @@ func TestToFilterArg(t *testing.T) { } var ( - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - testAddr = crypto.PubkeyToAddress(testKey.PublicKey) - testBalance = big.NewInt(2e10) + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + testBalance = big.NewInt(2e10) + testBlockNum = 128 + testBlocks = []testBlockParam{ + { + // This txs params also used to default block. + blockNr: 10, + txs: []testTransactionParam{}, + }, + { + blockNr: 11, + txs: []testTransactionParam{ + { + to: common.Address{0x01}, + value: big.NewInt(1), + gasPrice: big.NewInt(1), + data: nil, + }, + }, + }, + { + blockNr: 12, + txs: []testTransactionParam{ + { + to: common.Address{0x01}, + value: big.NewInt(1), + gasPrice: big.NewInt(1), + data: nil, + }, + { + to: common.Address{0x02}, + value: big.NewInt(2), + gasPrice: big.NewInt(2), + data: nil, + }, + }, + }, + { + blockNr: 13, + txs: []testTransactionParam{ + { + to: common.Address{0x01}, + value: big.NewInt(1), + gasPrice: big.NewInt(1), + data: nil, + }, + { + to: common.Address{0x02}, + value: big.NewInt(2), + gasPrice: big.NewInt(2), + data: nil, + }, + { + to: common.Address{0x03}, + value: big.NewInt(3), + gasPrice: big.NewInt(3), + data: nil, + }, + }, + }, + } ) +type testTransactionParam struct { + to common.Address + value *big.Int + gasPrice *big.Int + data []byte +} + +type testBlockParam struct { + blockNr int + txs []testTransactionParam +} + func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { // Generate test chain. genesis, blocks := generateTestChain() @@ -197,6 +270,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { // Create Ethereum Service config := ðconfig.Config{Genesis: genesis} config.Ethash.PowMode = ethash.ModeFake + config.SnapshotCache = 256 ethservice, err := eth.New(n, config) if err != nil { t.Fatalf("can't create new ethereum service: %v", err) @@ -212,7 +286,10 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { } func generateTestChain() (*core.Genesis, []*types.Block) { + signer := types.HomesteadSigner{} + // Create a database pre-initialize with a genesis block db := rawdb.NewMemoryDatabase() + db.SetDiffStore(memorydb.New()) config := params.AllEthashProtocolChanges genesis := &core.Genesis{ Config: config, @@ -220,13 +297,45 @@ func generateTestChain() (*core.Genesis, []*types.Block) { ExtraData: []byte("test genesis"), Timestamp: 9000, } - generate := func(i int, g *core.BlockGen) { - g.OffsetTime(5) - g.SetExtra([]byte("test")) + genesis.MustCommit(db) + chain, _ := core.NewBlockChain(db, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, nil, core.EnablePersistDiff(860000)) + generate := func(i int, block *core.BlockGen) { + block.OffsetTime(5) + block.SetExtra([]byte("test")) + //block.SetCoinbase(testAddr) + + for idx, testBlock := range testBlocks { + // Specific block setting, the index in this generator has 1 diff from specified blockNr. + if i+1 == testBlock.blockNr { + for _, testTransaction := range testBlock.txs { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), testTransaction.to, + testTransaction.value, params.TxGas, testTransaction.gasPrice, testTransaction.data), signer, testKey) + if err != nil { + panic(err) + } + block.AddTxWithChain(chain, tx) + } + break + } + + // Default block setting. + if idx == len(testBlocks)-1 { + // We want to simulate an empty middle block, having the same state as the + // first one. The last is needs a state change again to force a reorg. + for _, testTransaction := range testBlocks[0].txs { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddr), testTransaction.to, + testTransaction.value, params.TxGas, testTransaction.gasPrice, testTransaction.data), signer, testKey) + if err != nil { + panic(err) + } + block.AddTxWithChain(chain, tx) + } + } + } } gblock := genesis.ToBlock(db) engine := ethash.NewFaker() - blocks, _ := core.GenerateChain(config, gblock, engine, db, 1, generate) + blocks, _ := core.GenerateChain(config, gblock, engine, db, testBlockNum, generate) blocks = append([]*types.Block{gblock}, blocks...) return genesis, blocks } @@ -261,6 +370,9 @@ func TestEthClient(t *testing.T) { "TestCallContract": { func(t *testing.T) { testCallContract(t, client) }, }, + "TestDiffAccounts": { + func(t *testing.T) { testDiffAccounts(t, client) }, + }, // DO not have TestAtFunctions now, because we do not have pending block now } @@ -393,7 +505,7 @@ func testGetBlock(t *testing.T, client *rpc.Client) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if blockNumber != 1 { + if blockNumber != uint64(testBlockNum) { t.Fatalf("BlockNumber returned wrong number: %d", blockNumber) } // Get current block by number @@ -507,3 +619,44 @@ func sendTransaction(ec *Client) error { // Send transaction return ec.SendTransaction(context.Background(), signedTx) } + +func testDiffAccounts(t *testing.T, client *rpc.Client) { + ec := NewClient(client) + ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond) + defer cancel() + + for _, testBlock := range testBlocks { + if testBlock.blockNr == 10 { + continue + } + diffAccounts, err := ec.GetDiffAccounts(ctx, big.NewInt(int64(testBlock.blockNr))) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + accounts := make([]common.Address, 0) + for _, tx := range testBlock.txs { + // tx.to should be in the accounts list. + for idx, account := range diffAccounts { + if tx.to == account { + break + } + + if idx == len(diffAccounts)-1 { + t.Fatalf("address(%v) expected in the diff account list, but not", tx.to) + } + } + + accounts = append(accounts, tx.to) + } + + diffDetail, err := ec.GetDiffAccountsWithScope(ctx, big.NewInt(int64(testBlock.blockNr)), accounts) + if err != nil { + t.Fatalf("get diff accounts in block error: %v", err) + } + // No contract deposit tx, so expect empty transactions. + if len(diffDetail.Transactions) != 0 { + t.Fatalf("expect ignore all transactions, but some transaction has recorded") + } + } +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 15e7a8c8f1..e6064ab787 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/common/gopool" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" @@ -1086,6 +1087,111 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs, bl return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) } +// GetDiffAccounts returns changed accounts in a specific block number. +func (s *PublicBlockChainAPI) GetDiffAccounts(ctx context.Context, blockNr rpc.BlockNumber) ([]common.Address, error) { + if s.b.Chain() == nil { + return nil, fmt.Errorf("blockchain not support get diff accounts") + } + + header, err := s.b.HeaderByNumber(ctx, blockNr) + if err != nil { + return nil, fmt.Errorf("block not found for block number (%d): %v", blockNr, err) + } + + return s.b.Chain().GetDiffAccounts(header.Hash()) +} + +// GetDiffAccountsWithScope returns detailed changes of some interested accounts in a specific block number. +func (s *PublicBlockChainAPI) GetDiffAccountsWithScope(ctx context.Context, blockNr rpc.BlockNumber, accounts []common.Address) (*types.DiffAccountsInBlock, error) { + if s.b.Chain() == nil { + return nil, fmt.Errorf("blockchain not support get diff accounts") + } + + block, err := s.b.BlockByNumber(ctx, blockNr) + if err != nil { + return nil, fmt.Errorf("block not found for block number (%d): %v", blockNr, err) + } + parent, err := s.b.BlockByHash(ctx, block.ParentHash()) + if err != nil { + return nil, fmt.Errorf("block not found for block number (%d): %v", blockNr-1, err) + } + statedb, err := s.b.Chain().StateAt(parent.Root()) + if err != nil { + return nil, fmt.Errorf("state not found for block number (%d): %v", blockNr-1, err) + } + + result := &types.DiffAccountsInBlock{ + Number: uint64(blockNr), + BlockHash: block.Hash(), + Transactions: make([]types.DiffAccountsInTx, 0), + } + + accountSet := make(map[common.Address]struct{}, len(accounts)) + for _, account := range accounts { + accountSet[account] = struct{}{} + } + + // Recompute transactions. + signer := types.MakeSigner(s.b.ChainConfig(), block.Number()) + for _, tx := range block.Transactions() { + // Skip data empty tx and to is one of the interested accounts tx. + skip := false + if len(tx.Data()) == 0 { + skip = true + } else if to := tx.To(); to != nil { + if _, exists := accountSet[*to]; exists { + skip = true + } + } + + diffTx := types.DiffAccountsInTx{ + TxHash: tx.Hash(), + Accounts: make(map[common.Address]*big.Int, len(accounts)), + } + + if !skip { + // Record account balance + for _, account := range accounts { + diffTx.Accounts[account] = statedb.GetBalance(account) + } + } + + // Apply transaction + msg, _ := tx.AsMessage(signer) + txContext := core.NewEVMTxContext(msg) + context := core.NewEVMBlockContext(block.Header(), s.b.Chain(), nil) + vmenv := vm.NewEVM(context, txContext, statedb, s.b.ChainConfig(), vm.Config{}) + + if posa, ok := s.b.Engine().(consensus.PoSA); ok { + if isSystem, _ := posa.IsSystemTransaction(tx, block.Header()); isSystem { + balance := statedb.GetBalance(consensus.SystemAddress) + if balance.Cmp(common.Big0) > 0 { + statedb.SetBalance(consensus.SystemAddress, big.NewInt(0)) + statedb.AddBalance(block.Header().Coinbase, balance) + } + } + } + + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + return nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) + } + statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) + + if !skip { + // Compute account balance diff. + for _, account := range accounts { + diffTx.Accounts[account] = new(big.Int).Sub(statedb.GetBalance(account), diffTx.Accounts[account]) + if diffTx.Accounts[account].Cmp(big.NewInt(0)) == 0 { + delete(diffTx.Accounts, account) + } + } + result.Transactions = append(result.Transactions, diffTx) + } + } + + return result, nil +} + // ExecutionResult groups all structured logs emitted by the EVM // while replaying a transaction in debug mode as well as transaction // execution status, the amount of gas used and the return value diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 07e76583f3..ca5a55d5ed 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -42,6 +42,7 @@ type Backend interface { // General Ethereum API Downloader() *downloader.Downloader SuggestPrice(ctx context.Context) (*big.Int, error) + Chain() *core.BlockChain ChainDb() ethdb.Database AccountManager() *accounts.Manager ExtRPCEnabled() bool diff --git a/les/api_backend.go b/les/api_backend.go index 60c64a8bdf..c8eca2d905 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -255,6 +255,10 @@ func (b *LesApiBackend) SuggestPrice(ctx context.Context) (*big.Int, error) { return b.gpo.SuggestPrice(ctx) } +func (b *LesApiBackend) Chain() *core.BlockChain { + return nil +} + func (b *LesApiBackend) ChainDb() ethdb.Database { return b.eth.chainDb }