From 77442418865a58ae5888e4b7113608031a237006 Mon Sep 17 00:00:00 2001 From: holisticode Date: Sat, 11 May 2019 05:55:06 -0500 Subject: [PATCH 01/34] swarm/network/stream: add pure retrieval test (#19552) --- .../network/stream/snapshot_retrieval_test.go | 273 ++++++++++++++---- 1 file changed, 218 insertions(+), 55 deletions(-) diff --git a/swarm/network/stream/snapshot_retrieval_test.go b/swarm/network/stream/snapshot_retrieval_test.go index e34f87951b..50617b5cf5 100644 --- a/swarm/network/stream/snapshot_retrieval_test.go +++ b/swarm/network/stream/snapshot_retrieval_test.go @@ -16,8 +16,10 @@ package stream import ( + "bytes" "context" "fmt" + "io" "sync" "testing" "time" @@ -33,17 +35,17 @@ import ( "github.com/ethereum/go-ethereum/swarm/testutil" ) -//constants for random file generation +// constants for random file generation const ( minFileSize = 2 maxFileSize = 40 ) -//This test is a retrieval test for nodes. -//A configurable number of nodes can be -//provided to the test. -//Files are uploaded to nodes, other nodes try to retrieve the file -//Number of nodes can be provided via commandline too. +// TestFileRetrieval is a retrieval test for nodes. +// A configurable number of nodes can be +// provided to the test. +// Files are uploaded to nodes, other nodes try to retrieve the file +// Number of nodes can be provided via commandline too. func TestFileRetrieval(t *testing.T) { var nodeCount []int @@ -61,26 +63,57 @@ func TestFileRetrieval(t *testing.T) { } for _, nc := range nodeCount { - if err := runFileRetrievalTest(nc); err != nil { - t.Error(err) + runFileRetrievalTest(t, nc) + } +} + +// TestPureRetrieval tests pure retrieval without syncing +// A configurable number of nodes and chunks +// can be provided to the test. +// A number of random chunks is generated, then stored directly in +// each node's localstore according to their address. +// Each chunk is supposed to end up at certain nodes +// With retrieval we then make sure that every node can actually retrieve +// the chunks. +func TestPureRetrieval(t *testing.T) { + var nodeCount []int + var chunkCount []int + + if *nodes != 0 && *chunks != 0 { + nodeCount = []int{*nodes} + chunkCount = []int{*chunks} + } else { + nodeCount = []int{16} + chunkCount = []int{150} + + if *longrunning { + nodeCount = append(nodeCount, 32, 64) + chunkCount = append(chunkCount, 32, 256) + } else if testutil.RaceEnabled { + nodeCount = []int{4} + chunkCount = []int{4} + } + + } + + for _, nc := range nodeCount { + for _, c := range chunkCount { + runPureRetrievalTest(t, nc, c) } } } -//This test is a retrieval test for nodes. -//One node is randomly selected to be the pivot node. -//A configurable number of chunks and nodes can be -//provided to the test, the number of chunks is uploaded -//to the pivot node and other nodes try to retrieve the chunk(s). -//Number of chunks and nodes can be provided via commandline too. +// TestRetrieval tests retrieval of chunks by random nodes. +// One node is randomly selected to be the pivot node. +// A configurable number of chunks and nodes can be +// provided to the test, the number of chunks is uploaded +// to the pivot node and other nodes try to retrieve the chunk(s). +// Number of chunks and nodes can be provided via commandline too. func TestRetrieval(t *testing.T) { - //if nodes/chunks have been provided via commandline, - //run the tests with these values + // if nodes/chunks have been provided via commandline, + // run the tests with these values if *nodes != 0 && *chunks != 0 { - err := runRetrievalTest(t, *chunks, *nodes) - if err != nil { - t.Fatal(err) - } + runRetrievalTest(t, *chunks, *nodes) } else { nodeCnt := []int{16} chnkCnt := []int{32} @@ -96,10 +129,7 @@ func TestRetrieval(t *testing.T) { for _, n := range nodeCnt { for _, c := range chnkCnt { t.Run(fmt.Sprintf("TestRetrieval_%d_%d", n, c), func(t *testing.T) { - err := runRetrievalTest(t, c, n) - if err != nil { - t.Fatal(err) - } + runRetrievalTest(t, c, n) }) } } @@ -132,15 +162,160 @@ var retrievalSimServiceMap = map[string]simulation.ServiceFunc{ }, } -/* -The test loads a snapshot file to construct the swarm network, -assuming that the snapshot file identifies a healthy -kademlia network. Nevertheless a health check runs in the -simulation's `action` function. +// runPureRetrievalTest by uploading a snapshot, +// then starting a simulation, distribute chunks to nodes +// and start retrieval. +// The snapshot should have 'streamer' in its service list. +func runPureRetrievalTest(t *testing.T, nodeCount int, chunkCount int) { + + t.Helper() + // the pure retrieval test needs a different service map, as we want + // syncing disabled and we don't need to set the syncUpdateDelay + sim := simulation.New(map[string]simulation.ServiceFunc{ + "streamer": func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error) { + addr, netStore, delivery, clean, err := newNetStoreAndDelivery(ctx, bucket) + if err != nil { + return nil, nil, err + } + + r := NewRegistry(addr.ID(), delivery, netStore, state.NewInmemoryStore(), &RegistryOptions{ + Syncing: SyncingDisabled, + }, nil) + + cleanup = func() { + r.Close() + clean() + } + + return r, cleanup, nil + }, + }, + ) + defer sim.Close() + + log.Info("Initializing test config", "node count", nodeCount) + + conf := &synctestConfig{} + //map of discover ID to indexes of chunks expected at that ID + conf.idToChunksMap = make(map[enode.ID][]int) + //map of overlay address to discover ID + conf.addrToIDMap = make(map[string]enode.ID) + //array where the generated chunk hashes will be stored + conf.hashes = make([]storage.Address, 0) + + ctx, cancelSimRun := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancelSimRun() + + filename := fmt.Sprintf("testing/snapshot_%d.json", nodeCount) + err := sim.UploadSnapshot(ctx, filename) + if err != nil { + t.Fatal(err) + } + + log.Info("Starting simulation") + + result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { + nodeIDs := sim.UpNodeIDs() + // first iteration: create addresses + for _, n := range nodeIDs { + //get the kademlia overlay address from this ID + a := n.Bytes() + //append it to the array of all overlay addresses + conf.addrs = append(conf.addrs, a) + //the proximity calculation is on overlay addr, + //the p2p/simulations check func triggers on enode.ID, + //so we need to know which overlay addr maps to which nodeID + conf.addrToIDMap[string(a)] = n + } + + // now create random chunks + chunks := storage.GenerateRandomChunks(int64(chunkSize), chunkCount) + for _, chunk := range chunks { + conf.hashes = append(conf.hashes, chunk.Address()) + } + + log.Debug("random chunks generated, mapping keys to nodes") + + // map addresses to nodes + mapKeysToNodes(conf) + + // second iteration: storing chunks at the peer whose + // overlay address is closest to a particular chunk's hash + log.Debug("storing every chunk at correspondent node store") + for _, id := range nodeIDs { + // for every chunk for this node (which are only indexes)... + for _, ch := range conf.idToChunksMap[id] { + item, ok := sim.NodeItem(id, bucketKeyStore) + if !ok { + return fmt.Errorf("Error accessing localstore") + } + lstore := item.(chunk.Store) + // ...get the actual chunk + for _, chnk := range chunks { + if bytes.Equal(chnk.Address(), conf.hashes[ch]) { + // ...and store it in the localstore + if _, err = lstore.Put(ctx, chunk.ModePutUpload, chnk); err != nil { + return err + } + } + } + } + } + + // now try to retrieve every chunk from every node + log.Debug("starting retrieval") + cnt := 0 + + for _, id := range nodeIDs { + item, ok := sim.NodeItem(id, bucketKeyFileStore) + if !ok { + return fmt.Errorf("No filestore") + } + fileStore := item.(*storage.FileStore) + for _, chunk := range chunks { + reader, _ := fileStore.Retrieve(context.TODO(), chunk.Address()) + content := make([]byte, chunkSize) + size, err := reader.Read(content) + //check chunk size and content + ok := true + if err != io.EOF { + log.Debug("Retrieve error", "err", err, "hash", chunk.Address(), "nodeId", id) + ok = false + } + if size != chunkSize { + log.Debug("size not equal chunkSize", "size", size, "hash", chunk.Address(), "nodeId", id) + ok = false + } + // skip chunk "metadata" for chunk.Data() + if !bytes.Equal(content, chunk.Data()[8:]) { + log.Debug("content not equal chunk data", "hash", chunk.Address(), "nodeId", id) + ok = false + } + if !ok { + return fmt.Errorf("Expected test to succeed at first run, but failed with chunk not found") + } + log.Debug(fmt.Sprintf("chunk with root hash %x successfully retrieved", chunk.Address())) + cnt++ + } + } + log.Info("retrieval terminated, chunks retrieved: ", "count", cnt) + return nil + + }) + + log.Info("Simulation terminated") + + if result.Error != nil { + t.Fatal(result.Error) + } +} + +// runFileRetrievalTest loads a snapshot file to construct the swarm network. +// The snapshot should have 'streamer' in its service list. +func runFileRetrievalTest(t *testing.T, nodeCount int) { + + t.Helper() -The snapshot should have 'streamer' in its service list. -*/ -func runFileRetrievalTest(nodeCount int) error { sim := simulation.New(retrievalSimServiceMap) defer sim.Close() @@ -160,7 +335,7 @@ func runFileRetrievalTest(nodeCount int) error { filename := fmt.Sprintf("testing/snapshot_%d.json", nodeCount) err := sim.UploadSnapshot(ctx, filename) if err != nil { - return err + t.Fatal(err) } log.Info("Starting simulation") @@ -180,9 +355,6 @@ func runFileRetrievalTest(nodeCount int) error { //an array for the random files var randomFiles []string - //channel to signal when the upload has finished - //uploadFinished := make(chan struct{}) - //channel to trigger new node checks conf.hashes, randomFiles, err = uploadFilesToNodes(sim) if err != nil { @@ -221,24 +393,17 @@ func runFileRetrievalTest(nodeCount int) error { log.Info("Simulation terminated") if result.Error != nil { - return result.Error + t.Fatal(result.Error) } - - return nil } -/* -The test generates the given number of chunks. +// runRetrievalTest generates the given number of chunks. +// The test loads a snapshot file to construct the swarm network. +// The snapshot should have 'streamer' in its service list. +func runRetrievalTest(t *testing.T, chunkCount int, nodeCount int) { -The test loads a snapshot file to construct the swarm network, -assuming that the snapshot file identifies a healthy -kademlia network. Nevertheless a health check runs in the -simulation's `action` function. - -The snapshot should have 'streamer' in its service list. -*/ -func runRetrievalTest(t *testing.T, chunkCount int, nodeCount int) error { t.Helper() + sim := simulation.New(retrievalSimServiceMap) defer sim.Close() @@ -256,7 +421,7 @@ func runRetrievalTest(t *testing.T, chunkCount int, nodeCount int) error { filename := fmt.Sprintf("testing/snapshot_%d.json", nodeCount) err := sim.UploadSnapshot(ctx, filename) if err != nil { - return err + t.Fatal(err) } result := sim.Run(ctx, func(ctx context.Context, sim *simulation.Simulation) error { @@ -278,8 +443,8 @@ func runRetrievalTest(t *testing.T, chunkCount int, nodeCount int) error { if !ok { return fmt.Errorf("No localstore") } - store := item.(chunk.Store) - conf.hashes, err = uploadFileToSingleNodeStore(node.ID(), chunkCount, store) + lstore := item.(chunk.Store) + conf.hashes, err = uploadFileToSingleNodeStore(node.ID(), chunkCount, lstore) if err != nil { return err } @@ -314,8 +479,6 @@ func runRetrievalTest(t *testing.T, chunkCount int, nodeCount int) error { }) if result.Error != nil { - return result.Error + t.Fatal(result.Error) } - - return nil } From c8a77d8604bd4295e0a0f88b7165e7729be78409 Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Mon, 13 May 2019 09:55:11 +0200 Subject: [PATCH 02/34] swarm/metrics: track runtime metrics (#19557) --- swarm/metrics/flags.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/swarm/metrics/flags.go b/swarm/metrics/flags.go index d348dc3e4d..3e7918b168 100644 --- a/swarm/metrics/flags.go +++ b/swarm/metrics/flags.go @@ -20,6 +20,7 @@ import ( "time" "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/metrics" gethmetrics "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics/influxdb" "github.com/ethereum/go-ethereum/swarm/log" @@ -91,7 +92,10 @@ func Setup(ctx *cli.Context) { ) // Start system runtime metrics collection - go gethmetrics.CollectProcessMetrics(2 * time.Second) + go gethmetrics.CollectProcessMetrics(4 * time.Second) + + gethmetrics.RegisterRuntimeMemStats(metrics.DefaultRegistry) + go gethmetrics.CaptureRuntimeMemStats(metrics.DefaultRegistry, 4*time.Second) tagsMap := utils.SplitTagsFlag(ctx.GlobalString(MetricsInfluxDBTagsFlag.Name)) From db83ba4067e1b9f2fa13045c1e8cdbf68917595e Mon Sep 17 00:00:00 2001 From: Elad Date: Mon, 13 May 2019 12:27:27 +0400 Subject: [PATCH 03/34] cmd/swarm: skip export test on windows builds (#19555) --- cmd/swarm/export_test.go | 6 +++++- swarm/network/simulation/kademlia_test.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/swarm/export_test.go b/cmd/swarm/export_test.go index 19e54c21dd..ca82cfd4c9 100644 --- a/cmd/swarm/export_test.go +++ b/cmd/swarm/export_test.go @@ -123,6 +123,9 @@ func TestCLISwarmExportImport(t *testing.T) { // 5. import the dump // 6. file should be accessible func TestExportLegacyToNew(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() // this should be reenabled once the appveyor tests underlying issue is fixed + } /* fixture bzz account 0aa159029fa13ffa8fa1c6fff6ebceface99d6a4 */ @@ -170,7 +173,7 @@ func TestExportLegacyToNew(t *testing.T) { if stat.Size() < 90000 { t.Fatal("export size too small") } - t.Log("removing chunk datadir") + log.Info("removing chunk datadir") err = os.RemoveAll(path.Join(actualDataDir, "chunks")) if err != nil { t.Fatal(err) @@ -276,6 +279,7 @@ func inflateBase64Gzip(t *testing.T, base64File, directory string) { if _, err := io.Copy(file, tarReader); err != nil { t.Fatal(err) } + file.Close() default: t.Fatal("shouldn't happen") } diff --git a/swarm/network/simulation/kademlia_test.go b/swarm/network/simulation/kademlia_test.go index 4d7dc62404..af50aacfd9 100644 --- a/swarm/network/simulation/kademlia_test.go +++ b/swarm/network/simulation/kademlia_test.go @@ -42,7 +42,7 @@ import ( * If all kademlias are healthy, the test succeeded, otherwise it failed */ func TestWaitTillHealthy(t *testing.T) { - + t.Skip("this test is flaky; disabling till underlying problem is solved") testNodesNum := 10 // create the first simulation From 95263914fcc6631aedc7294624ae39c2adf3cb5f Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 13 May 2019 11:25:54 +0200 Subject: [PATCH 04/34] p2p/discover: fix a race where table loop would self-lookup before returning from constructor --- p2p/discover/table.go | 1 - p2p/discover/table_util_test.go | 1 + p2p/discover/v4_udp.go | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 8afe77bf18..61c62f1878 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -119,7 +119,6 @@ func newTable(t transport, db *enode.DB, bootnodes []*enode.Node, log log.Logger tab.seedRand() tab.loadSeedNodes() - go tab.loop() return tab, nil } diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 811466cf7f..71cb1895b0 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -42,6 +42,7 @@ func init() { func newTestTable(t transport) (*Table, *enode.DB) { db, _ := enode.OpenDB("") tab, _ := newTable(t, db, nil, log.Root()) + go tab.loop() return tab, db } diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index cdd42c38a0..3c68beac1c 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -253,6 +253,7 @@ func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) { return nil, err } t.tab = tab + go tab.loop() t.wg.Add(2) go t.loop() From ec2131c8d3382db3762909a8e7d225be61bae230 Mon Sep 17 00:00:00 2001 From: PilkyuJung Date: Mon, 13 May 2019 19:23:32 +0900 Subject: [PATCH 05/34] core: move error variable to error.go (#19560) * move error variable to error.go * Update error.go Edit "Genesis" to "genesis" --- core/blockchain.go | 4 +--- core/error.go | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 52d1a7c29b..c93c71403e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -62,9 +62,7 @@ var ( blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil) blockPrefetchExecuteTimer = metrics.NewRegisteredTimer("chain/prefetch/executes", nil) - blockPrefetchInterruptMeter = metrics.NewRegisteredMeter("chain/prefetch/interrupts", nil) - - ErrNoGenesis = errors.New("Genesis not found in chain") + blockPrefetchInterruptMeter = metrics.NewRegisteredMeter("chain/prefetch/interrupts", nil) ) const ( diff --git a/core/error.go b/core/error.go index 410eca1e1e..cd4be3d705 100644 --- a/core/error.go +++ b/core/error.go @@ -32,4 +32,7 @@ var ( // ErrNonceTooHigh is returned if the nonce of a transaction is higher than the // next one expected based on the local chain. ErrNonceTooHigh = errors.New("nonce too high") + + // ErrNoGenesis is returned when there is no Genesis Block. + ErrNoGenesis = errors.New("genesis not found in chain") ) From 751effa35e1e538093da4e8f5f466202639065a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 13 May 2019 14:07:55 +0300 Subject: [PATCH 06/34] core: fix formatting error (trailing whitepace) --- core/blockchain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/blockchain.go b/core/blockchain.go index c93c71403e..61b809319c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -62,7 +62,7 @@ var ( blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil) blockPrefetchExecuteTimer = metrics.NewRegisteredTimer("chain/prefetch/executes", nil) - blockPrefetchInterruptMeter = metrics.NewRegisteredMeter("chain/prefetch/interrupts", nil) + blockPrefetchInterruptMeter = metrics.NewRegisteredMeter("chain/prefetch/interrupts", nil) ) const ( From f4fb1a18015d88aa7b424709aac346da59edf410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?= Date: Mon, 13 May 2019 13:26:47 +0200 Subject: [PATCH 07/34] les: fixed cost table update (#19546) --- les/costtracker.go | 10 ++++++---- les/peer.go | 9 ++++++--- les/peer_test.go | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/les/costtracker.go b/les/costtracker.go index 014b888c01..332d733431 100644 --- a/les/costtracker.go +++ b/les/costtracker.go @@ -346,12 +346,14 @@ func (table requestCostTable) getCost(code, amount uint64) uint64 { } // decode converts a cost list to a cost table -func (list RequestCostList) decode() requestCostTable { +func (list RequestCostList) decode(protocolLength uint64) requestCostTable { table := make(requestCostTable) for _, e := range list { - table[e.MsgCode] = &requestCosts{ - baseCost: e.BaseCost, - reqCost: e.ReqCost, + if e.MsgCode < protocolLength { + table[e.MsgCode] = &requestCosts{ + baseCost: e.BaseCost, + reqCost: e.ReqCost, + } } } return table diff --git a/les/peer.go b/les/peer.go index a8aad3cd0b..7a163cd1d0 100644 --- a/les/peer.go +++ b/les/peer.go @@ -479,7 +479,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis costList = testCostList() } send = send.add("flowControl/MRC", costList) - p.fcCosts = costList.decode() + p.fcCosts = costList.decode(ProtocolLengths[uint(p.version)]) p.fcParams = server.defParams } else { //on client node @@ -571,7 +571,7 @@ func (p *peer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis } p.fcParams = params p.fcServer = flowcontrol.NewServerNode(params, &mclock.System{}) - p.fcCosts = MRC.decode() + p.fcCosts = MRC.decode(ProtocolLengths[uint(p.version)]) if !p.isOnlyAnnounce { for msgCode := range reqAvgTimeCost { if p.fcCosts[msgCode] == nil { @@ -604,7 +604,10 @@ func (p *peer) updateFlowControl(update keyValueMap) { } var MRC RequestCostList if update.get("flowControl/MRC", &MRC) == nil { - p.fcCosts = MRC.decode() + costUpdate := MRC.decode(ProtocolLengths[uint(p.version)]) + for code, cost := range costUpdate { + p.fcCosts[code] = cost + } } } diff --git a/les/peer_test.go b/les/peer_test.go index c5a7c238c2..62adf90fe1 100644 --- a/les/peer_test.go +++ b/les/peer_test.go @@ -13,7 +13,7 @@ import ( const ( test_networkid = 10 - protocol_version = 2123 + protocol_version = lpv2 ) var ( From 40cdcf8c47ff094775aca08fd5d94051f9cf1dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?= Date: Mon, 13 May 2019 13:41:10 +0200 Subject: [PATCH 08/34] les, light: implement ODR transaction lookup by hash (#19069) * les, light: implement ODR transaction lookup by hash * les: delete useless file * internal/ethapi: always use backend to find transaction * les, eth, internal/ethapi: renamed GetCanonicalTransaction to GetTransaction * light: add canonical header verification to GetTransaction --- eth/api_backend.go | 6 +++++ internal/ethapi/api.go | 23 ++++++++++++------- internal/ethapi/backend.go | 1 + les/api_backend.go | 4 ++++ les/backend.go | 3 ++- les/handler.go | 17 +++++++++----- les/handler_test.go | 26 +++++++++++----------- les/helper_test.go | 4 +++- les/odr.go | 1 + les/odr_requests.go | 40 +++++++++++++++++++++++++++++++++ les/odr_test.go | 45 +++++++++++++++++++++++++++++--------- les/peer.go | 2 +- les/protocol.go | 8 ------- les/txrelay.go | 15 +++++++++---- light/odr.go | 17 ++++++++++++++ light/odr_util.go | 21 ++++++++++++++++++ 16 files changed, 182 insertions(+), 51 deletions(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index 9ac06ffa4f..db0e8cf410 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/bloombits" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -178,6 +179,11 @@ func (b *EthAPIBackend) GetPoolTransaction(hash common.Hash) *types.Transaction return b.eth.txPool.Get(hash) } +func (b *EthAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.eth.ChainDb(), txHash) + return tx, blockHash, blockNumber, index, nil +} + func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { return b.eth.txPool.State().GetNonce(addr), nil } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index e918df9bbb..07142d66be 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1155,25 +1155,32 @@ func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, addr } // GetTransactionByHash returns the transaction for the given hash -func (s *PublicTransactionPoolAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) *RPCTransaction { +func (s *PublicTransactionPoolAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*RPCTransaction, error) { // Try to return an already finalized transaction - if tx, blockHash, blockNumber, index := rawdb.ReadTransaction(s.b.ChainDb(), hash); tx != nil { - return newRPCTransaction(tx, blockHash, blockNumber, index) + tx, blockHash, blockNumber, index, err := s.b.GetTransaction(ctx, hash) + if err != nil { + return nil, err + } + if tx != nil { + return newRPCTransaction(tx, blockHash, blockNumber, index), nil } // No finalized transaction, try to retrieve it from the pool if tx := s.b.GetPoolTransaction(hash); tx != nil { - return newRPCPendingTransaction(tx) + return newRPCPendingTransaction(tx), nil } + // Transaction unknown, return as such - return nil + return nil, nil } // GetRawTransactionByHash returns the bytes of the transaction for the given hash. func (s *PublicTransactionPoolAPI) GetRawTransactionByHash(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) { - var tx *types.Transaction - // Retrieve a finalized transaction, or a pooled otherwise - if tx, _, _, _ = rawdb.ReadTransaction(s.b.ChainDb(), hash); tx == nil { + tx, _, _, _, err := s.b.GetTransaction(ctx, hash) + if err != nil { + return nil, err + } + if tx == nil { if tx = s.b.GetPoolTransaction(hash); tx == nil { // Transaction not found anywhere, abort return nil, nil diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 0c6c7eace8..9229ccfb64 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -62,6 +62,7 @@ type Backend interface { // TxPool API SendTx(ctx context.Context, signedTx *types.Transaction) error + GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) GetPoolTransactions() (types.Transactions, error) GetPoolTransaction(txHash common.Hash) *types.Transaction GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) diff --git a/les/api_backend.go b/les/api_backend.go index 589cf572d2..6de15e7bd2 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -132,6 +132,10 @@ func (b *LesApiBackend) GetPoolTransaction(txHash common.Hash) *types.Transactio return b.eth.txPool.GetTransaction(txHash) } +func (b *LesApiBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + return light.GetTransaction(ctx, b.eth.odr, txHash) +} + func (b *LesApiBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { return b.eth.txPool.GetNonce(ctx, addr) } diff --git a/les/backend.go b/les/backend.go index 80a912816f..887f882108 100644 --- a/les/backend.go +++ b/les/backend.go @@ -114,9 +114,9 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { if leth.config.ULC != nil { trustedNodes = leth.config.ULC.TrustedServers } - leth.relay = NewLesTxRelay(peers, leth.reqDist) leth.serverPool = newServerPool(chainDb, quitSync, &leth.wg, trustedNodes) leth.retriever = newRetrieveManager(peers, leth.reqDist, leth.serverPool) + leth.relay = NewLesTxRelay(peers, leth.retriever) leth.odr = NewLesOdr(chainDb, light.DefaultClientIndexerConfig, leth.retriever) leth.chtIndexer = light.NewChtIndexer(chainDb, leth.odr, params.CHTFrequency, params.HelperTrieConfirmations) @@ -271,6 +271,7 @@ func (s *LightEthereum) Start(srvr *p2p.Server) error { // Ethereum protocol. func (s *LightEthereum) Stop() error { s.odr.Stop() + s.relay.Stop() s.bloomIndexer.Close() s.chtIndexer.Close() s.blockchain.Stop() diff --git a/les/handler.go b/les/handler.go index d46eeb03a5..c6baeece4d 100644 --- a/les/handler.go +++ b/les/handler.go @@ -979,7 +979,7 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { return errResp(ErrRequestRejected, "") } go func() { - stats := make([]txStatus, len(req.Txs)) + stats := make([]light.TxStatus, len(req.Txs)) for i, tx := range req.Txs { if i != 0 && !task.waitOrStop() { return @@ -1014,7 +1014,7 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { return errResp(ErrRequestRejected, "") } go func() { - stats := make([]txStatus, len(req.Hashes)) + stats := make([]light.TxStatus, len(req.Hashes)) for i, hash := range req.Hashes { if i != 0 && !task.waitOrStop() { return @@ -1032,7 +1032,7 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { p.Log().Trace("Received tx status response") var resp struct { ReqID, BV uint64 - Status []txStatus + Status []light.TxStatus } if err := msg.Decode(&resp); err != nil { return errResp(ErrDecode, "msg %v: %v", msg, err) @@ -1040,6 +1040,13 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { p.fcServer.ReceivedReply(resp.ReqID, resp.BV) + p.Log().Trace("Received helper trie proof response") + deliverMsg = &Msg{ + MsgType: MsgTxStatus, + ReqID: resp.ReqID, + Obj: resp.Status, + } + default: p.Log().Trace("Received unknown message", "code", msg.Code) return errResp(ErrInvalidMsgCode, "%v", msg.Code) @@ -1097,8 +1104,8 @@ func (pm *ProtocolManager) getHelperTrieAuxData(req HelperTrieReq) []byte { return nil } -func (pm *ProtocolManager) txStatus(hash common.Hash) txStatus { - var stat txStatus +func (pm *ProtocolManager) txStatus(hash common.Hash) light.TxStatus { + var stat light.TxStatus stat.Status = pm.txpool.Status([]common.Hash{hash})[0] // If the transaction is unknown to the pool, try looking it up locally if stat.Status == core.TxStatusUnknown { diff --git a/les/handler_test.go b/les/handler_test.go index c659f8088b..d8051c1450 100644 --- a/les/handler_test.go +++ b/les/handler_test.go @@ -443,7 +443,7 @@ func TestTransactionStatusLes2(t *testing.T) { var reqID uint64 - test := func(tx *types.Transaction, send bool, expStatus txStatus) { + test := func(tx *types.Transaction, send bool, expStatus light.TxStatus) { reqID++ if send { cost := peer.GetRequestCost(SendTxV2Msg, 1) @@ -452,7 +452,7 @@ func TestTransactionStatusLes2(t *testing.T) { cost := peer.GetRequestCost(GetTxStatusMsg, 1) sendRequest(peer.app, GetTxStatusMsg, reqID, cost, []common.Hash{tx.Hash()}) } - if err := expectResponse(peer.app, TxStatusMsg, reqID, testBufLimit, []txStatus{expStatus}); err != nil { + if err := expectResponse(peer.app, TxStatusMsg, reqID, testBufLimit, []light.TxStatus{expStatus}); err != nil { t.Errorf("transaction status mismatch") } } @@ -461,20 +461,20 @@ func TestTransactionStatusLes2(t *testing.T) { // test error status by sending an underpriced transaction tx0, _ := types.SignTx(types.NewTransaction(0, acc1Addr, big.NewInt(10000), params.TxGas, nil, nil), signer, testBankKey) - test(tx0, true, txStatus{Status: core.TxStatusUnknown, Error: core.ErrUnderpriced.Error()}) + test(tx0, true, light.TxStatus{Status: core.TxStatusUnknown, Error: core.ErrUnderpriced.Error()}) tx1, _ := types.SignTx(types.NewTransaction(0, acc1Addr, big.NewInt(10000), params.TxGas, big.NewInt(100000000000), nil), signer, testBankKey) - test(tx1, false, txStatus{Status: core.TxStatusUnknown}) // query before sending, should be unknown - test(tx1, true, txStatus{Status: core.TxStatusPending}) // send valid processable tx, should return pending - test(tx1, true, txStatus{Status: core.TxStatusPending}) // adding it again should not return an error + test(tx1, false, light.TxStatus{Status: core.TxStatusUnknown}) // query before sending, should be unknown + test(tx1, true, light.TxStatus{Status: core.TxStatusPending}) // send valid processable tx, should return pending + test(tx1, true, light.TxStatus{Status: core.TxStatusPending}) // adding it again should not return an error tx2, _ := types.SignTx(types.NewTransaction(1, acc1Addr, big.NewInt(10000), params.TxGas, big.NewInt(100000000000), nil), signer, testBankKey) tx3, _ := types.SignTx(types.NewTransaction(2, acc1Addr, big.NewInt(10000), params.TxGas, big.NewInt(100000000000), nil), signer, testBankKey) // send transactions in the wrong order, tx3 should be queued - test(tx3, true, txStatus{Status: core.TxStatusQueued}) - test(tx2, true, txStatus{Status: core.TxStatusPending}) + test(tx3, true, light.TxStatus{Status: core.TxStatusQueued}) + test(tx2, true, light.TxStatus{Status: core.TxStatusPending}) // query again, now tx3 should be pending too - test(tx3, false, txStatus{Status: core.TxStatusPending}) + test(tx3, false, light.TxStatus{Status: core.TxStatusPending}) // generate and add a block with tx1 and tx2 included gchain, _ := core.GenerateChain(params.TestChainConfig, chain.GetBlockByNumber(0), ethash.NewFaker(), db, 1, func(i int, block *core.BlockGen) { @@ -497,8 +497,8 @@ func TestTransactionStatusLes2(t *testing.T) { // check if their status is included now block1hash := rawdb.ReadCanonicalHash(db, 1) - test(tx1, false, txStatus{Status: core.TxStatusIncluded, Lookup: &rawdb.LegacyTxLookupEntry{BlockHash: block1hash, BlockIndex: 1, Index: 0}}) - test(tx2, false, txStatus{Status: core.TxStatusIncluded, Lookup: &rawdb.LegacyTxLookupEntry{BlockHash: block1hash, BlockIndex: 1, Index: 1}}) + test(tx1, false, light.TxStatus{Status: core.TxStatusIncluded, Lookup: &rawdb.LegacyTxLookupEntry{BlockHash: block1hash, BlockIndex: 1, Index: 0}}) + test(tx2, false, light.TxStatus{Status: core.TxStatusIncluded, Lookup: &rawdb.LegacyTxLookupEntry{BlockHash: block1hash, BlockIndex: 1, Index: 1}}) // create a reorg that rolls them back gchain, _ = core.GenerateChain(params.TestChainConfig, chain.GetBlockByNumber(0), ethash.NewFaker(), db, 2, func(i int, block *core.BlockGen) {}) @@ -516,6 +516,6 @@ func TestTransactionStatusLes2(t *testing.T) { t.Fatalf("pending count mismatch: have %d, want 3", pending) } // check if their status is pending again - test(tx1, false, txStatus{Status: core.TxStatusPending}) - test(tx2, false, txStatus{Status: core.TxStatusPending}) + test(tx1, false, light.TxStatus{Status: core.TxStatusPending}) + test(tx2, false, light.TxStatus{Status: core.TxStatusPending}) } diff --git a/les/helper_test.go b/les/helper_test.go index 020c69e175..9a302f8370 100644 --- a/les/helper_test.go +++ b/les/helper_test.go @@ -148,6 +148,7 @@ func newTestProtocolManager(lightSync bool, blocks int, generator func(int, *cor } genesis = gspec.MustCommit(db) chain BlockChain + pool txPool ) if peers == nil { peers = newPeerSet() @@ -162,13 +163,14 @@ func newTestProtocolManager(lightSync bool, blocks int, generator func(int, *cor panic(err) } chain = blockchain + pool = core.NewTxPool(core.DefaultTxPoolConfig, gspec.Config, blockchain) } indexConfig := light.TestServerIndexerConfig if lightSync { indexConfig = light.TestClientIndexerConfig } - pm, err := NewProtocolManager(gspec.Config, indexConfig, lightSync, NetworkId, evmux, engine, peers, chain, nil, db, odr, nil, nil, make(chan struct{}), new(sync.WaitGroup), ulcConfig) + pm, err := NewProtocolManager(gspec.Config, indexConfig, lightSync, NetworkId, evmux, engine, peers, chain, pool, db, odr, nil, nil, make(chan struct{}), new(sync.WaitGroup), ulcConfig) if err != nil { return nil, err } diff --git a/les/odr.go b/les/odr.go index daf2ea19ed..9176924cb8 100644 --- a/les/odr.go +++ b/les/odr.go @@ -86,6 +86,7 @@ const ( MsgReceipts MsgProofsV2 MsgHelperTrieProofs + MsgTxStatus ) // Msg encodes a LES message that delivers reply data for a request diff --git a/les/odr_requests.go b/les/odr_requests.go index 66d6175b8a..328750d7ab 100644 --- a/les/odr_requests.go +++ b/les/odr_requests.go @@ -66,6 +66,8 @@ func LesRequest(req light.OdrRequest) LesOdrRequest { return (*ChtRequest)(r) case *light.BloomRequest: return (*BloomRequest)(r) + case *light.TxStatusRequest: + return (*TxStatusRequest)(r) default: return nil } @@ -471,6 +473,44 @@ func (r *BloomRequest) Validate(db ethdb.Database, msg *Msg) error { return nil } +// TxStatusRequest is the ODR request type for transaction status +type TxStatusRequest light.TxStatusRequest + +// GetCost returns the cost of the given ODR request according to the serving +// peer's cost table (implementation of LesOdrRequest) +func (r *TxStatusRequest) GetCost(peer *peer) uint64 { + return peer.GetRequestCost(GetTxStatusMsg, len(r.Hashes)) +} + +// CanSend tells if a certain peer is suitable for serving the given request +func (r *TxStatusRequest) CanSend(peer *peer) bool { + return peer.version >= lpv2 +} + +// Request sends an ODR request to the LES network (implementation of LesOdrRequest) +func (r *TxStatusRequest) Request(reqID uint64, peer *peer) error { + peer.Log().Debug("Requesting transaction status", "count", len(r.Hashes)) + return peer.RequestTxStatus(reqID, r.GetCost(peer), r.Hashes) +} + +// Valid processes an ODR request reply message from the LES network +// returns true and stores results in memory if the message was a valid reply +// to the request (implementation of LesOdrRequest) +func (r *TxStatusRequest) Validate(db ethdb.Database, msg *Msg) error { + log.Debug("Validating transaction status", "count", len(r.Hashes)) + + // Ensure we have a correct message with a single block body + if msg.MsgType != MsgTxStatus { + return errInvalidMessageType + } + status := msg.Obj.([]light.TxStatus) + if len(status) != len(r.Hashes) { + return errInvalidEntryCount + } + r.Status = status + return nil +} + // readTraceDB stores the keys of database reads. We use this to check that received node // sets contain only the trie nodes necessary to make proofs pass. type readTraceDB struct { diff --git a/les/odr_test.go b/les/odr_test.go index 2cc28e3844..a1d5479563 100644 --- a/les/odr_test.go +++ b/les/odr_test.go @@ -38,7 +38,7 @@ import ( type odrTestFn func(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte -func TestOdrGetBlockLes2(t *testing.T) { testOdr(t, 2, 1, odrGetBlock) } +func TestOdrGetBlockLes2(t *testing.T) { testOdr(t, 2, 1, true, odrGetBlock) } func odrGetBlock(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { var block *types.Block @@ -54,7 +54,7 @@ func odrGetBlock(ctx context.Context, db ethdb.Database, config *params.ChainCon return rlp } -func TestOdrGetReceiptsLes2(t *testing.T) { testOdr(t, 2, 1, odrGetReceipts) } +func TestOdrGetReceiptsLes2(t *testing.T) { testOdr(t, 2, 1, true, odrGetReceipts) } func odrGetReceipts(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { var receipts types.Receipts @@ -74,7 +74,7 @@ func odrGetReceipts(ctx context.Context, db ethdb.Database, config *params.Chain return rlp } -func TestOdrAccountsLes2(t *testing.T) { testOdr(t, 2, 1, odrAccounts) } +func TestOdrAccountsLes2(t *testing.T) { testOdr(t, 2, 1, true, odrAccounts) } func odrAccounts(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { dummyAddr := common.HexToAddress("1234567812345678123456781234567812345678") @@ -102,7 +102,7 @@ func odrAccounts(ctx context.Context, db ethdb.Database, config *params.ChainCon return res } -func TestOdrContractCallLes2(t *testing.T) { testOdr(t, 2, 2, odrContractCall) } +func TestOdrContractCallLes2(t *testing.T) { testOdr(t, 2, 2, true, odrContractCall) } type callmsg struct { types.Message @@ -151,8 +151,32 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai return res } +func TestOdrTxStatusLes2(t *testing.T) { testOdr(t, 2, 1, false, odrTxStatus) } + +func odrTxStatus(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { + var txs types.Transactions + if bc != nil { + block := bc.GetBlockByHash(bhash) + txs = block.Transactions() + } else { + if block, _ := lc.GetBlockByHash(ctx, bhash); block != nil { + btxs := block.Transactions() + txs = make(types.Transactions, len(btxs)) + for i, tx := range btxs { + var err error + txs[i], _, _, _, err = light.GetTransaction(ctx, lc.Odr(), tx.Hash()) + if err != nil { + return nil + } + } + } + } + rlp, _ := rlp.EncodeToBytes(txs) + return rlp +} + // testOdr tests odr requests whose validation guaranteed by block headers. -func testOdr(t *testing.T, protocol int, expFail uint64, fn odrTestFn) { +func testOdr(t *testing.T, protocol int, expFail uint64, checkCached bool, fn odrTestFn) { // Assemble the test environment server, client, tearDown := newClientServerEnv(t, 4, protocol, nil, true) defer tearDown() @@ -193,9 +217,10 @@ func testOdr(t *testing.T, protocol int, expFail uint64, fn odrTestFn) { client.rPeer.hasBlock = func(common.Hash, uint64, bool) bool { return true } client.peers.lock.Unlock() test(5) - - // still expect all retrievals to pass, now data should be cached locally - client.peers.Unregister(client.rPeer.id) - time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed - test(5) + if checkCached { + // still expect all retrievals to pass, now data should be cached locally + client.peers.Unregister(client.rPeer.id) + time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed + test(5) + } } diff --git a/les/peer.go b/les/peer.go index 7a163cd1d0..42c13ab7d1 100644 --- a/les/peer.go +++ b/les/peer.go @@ -317,7 +317,7 @@ func (p *peer) ReplyHelperTrieProofs(reqID uint64, resp HelperTrieResps) *reply } // ReplyTxStatus creates a reply with a batch of transaction status records, corresponding to the ones requested. -func (p *peer) ReplyTxStatus(reqID uint64, stats []txStatus) *reply { +func (p *peer) ReplyTxStatus(reqID uint64, stats []light.TxStatus) *reply { data, _ := rlp.EncodeToBytes(stats) return &reply{p.rw, TxStatusMsg, reqID, data} } diff --git a/les/protocol.go b/les/protocol.go index 86e450d01c..24dc9bdeae 100644 --- a/les/protocol.go +++ b/les/protocol.go @@ -24,8 +24,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/rlp" @@ -227,9 +225,3 @@ type CodeData []struct { } type proofsData [][]rlp.RawValue - -type txStatus struct { - Status core.TxStatus - Lookup *rawdb.LegacyTxLookupEntry `rlp:"nil"` - Error string -} diff --git a/les/txrelay.go b/les/txrelay.go index a790bbec9c..5ebef1c226 100644 --- a/les/txrelay.go +++ b/les/txrelay.go @@ -17,6 +17,7 @@ package les import ( + "context" "sync" "github.com/ethereum/go-ethereum/common" @@ -36,21 +37,27 @@ type LesTxRelay struct { peerList []*peer peerStartPos int lock sync.RWMutex + stop chan struct{} - reqDist *requestDistributor + retriever *retrieveManager } -func NewLesTxRelay(ps *peerSet, reqDist *requestDistributor) *LesTxRelay { +func NewLesTxRelay(ps *peerSet, retriever *retrieveManager) *LesTxRelay { r := &LesTxRelay{ txSent: make(map[common.Hash]*ltrInfo), txPending: make(map[common.Hash]struct{}), ps: ps, - reqDist: reqDist, + retriever: retriever, + stop: make(chan struct{}), } ps.notify(r) return r } +func (self *LesTxRelay) Stop() { + close(self.stop) +} + func (self *LesTxRelay) registerPeer(p *peer) { self.lock.Lock() defer self.lock.Unlock() @@ -132,7 +139,7 @@ func (self *LesTxRelay) send(txs types.Transactions, count int) { return func() { peer.SendTxs(reqID, cost, enc) } }, } - self.reqDist.queue(rq) + go self.retriever.retrieve(context.Background(), reqID, rq, func(p distPeer, msg *Msg) error { return nil }, self.stop) } } diff --git a/light/odr.go b/light/odr.go index 95f1948e75..d1185e4e07 100644 --- a/light/odr.go +++ b/light/odr.go @@ -175,3 +175,20 @@ func (req *BloomRequest) StoreResult(db ethdb.Database) { rawdb.WriteBloomBits(db, req.BitIdx, sectionIdx, sectionHead, req.BloomBits[i]) } } + +// TxStatus describes the status of a transaction +type TxStatus struct { + Status core.TxStatus + Lookup *rawdb.LegacyTxLookupEntry `rlp:"nil"` + Error string +} + +// TxStatusRequest is the ODR request type for retrieving transaction status +type TxStatusRequest struct { + OdrRequest + Hashes []common.Hash + Status []TxStatus +} + +// StoreResult stores the retrieved data in local database +func (req *TxStatusRequest) StoreResult(db ethdb.Database) {} diff --git a/light/odr_util.go b/light/odr_util.go index cce532f8e7..100bd58428 100644 --- a/light/odr_util.go +++ b/light/odr_util.go @@ -21,6 +21,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "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/crypto" @@ -227,3 +228,23 @@ func GetBloomBits(ctx context.Context, odr OdrBackend, bitIdx uint, sectionIdxLi return result, nil } } + +// GetTransaction retrieves a canonical transaction by hash and also returns its position in the chain +func GetTransaction(ctx context.Context, odr OdrBackend, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + r := &TxStatusRequest{Hashes: []common.Hash{txHash}} + if err := odr.Retrieve(ctx, r); err != nil || r.Status[0].Status != core.TxStatusIncluded { + return nil, common.Hash{}, 0, 0, err + } else { + pos := r.Status[0].Lookup + // first ensure that we have the header, otherwise block body retrieval will fail + // also verify if this is a canonical block by getting the header by number and checking its hash + if header, err := GetHeaderByNumber(ctx, odr, pos.BlockIndex); err != nil || header.Hash() != pos.BlockHash { + return nil, common.Hash{}, 0, 0, err + } + if body, err := GetBody(ctx, odr, pos.BlockHash, pos.BlockIndex); err != nil || uint64(len(body.Transactions)) <= pos.Index || body.Transactions[pos.Index].Hash() != txHash { + return nil, common.Hash{}, 0, 0, err + } else { + return body.Transactions[pos.Index], pos.BlockHash, pos.BlockIndex, pos.Index, nil + } + } +} From f22c00b161aab64eb81f15f4e767aa9d9cd2cd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 13 May 2019 14:52:05 +0300 Subject: [PATCH 09/34] cmd/faucet: remove Google+ mention from web assets too --- cmd/faucet/faucet.html | 5 +--- cmd/faucet/website.go | 65 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/cmd/faucet/faucet.html b/cmd/faucet/faucet.html index ab41b2c871..314b19e123 100644 --- a/cmd/faucet/faucet.html +++ b/cmd/faucet/faucet.html @@ -80,14 +80,11 @@

How does this work?

-

This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter, Google+ or Facebook account may request funds within the permitted limits.

+

This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter or Facebook account may request funds within the permitted limits.

To request funds via Twitter, make a tweet with your Ethereum address pasted into the contents (surrounding text doesn't matter).
Copy-paste the tweets URL into the above input box and fire away!
-
-
To request funds via Google Plus, publish a new public post with your Ethereum address embedded into the content (surrounding text doesn't matter).
Copy-paste the posts URL into the above input box and fire away!
-
To request funds via Facebook, publish a new public post with your Ethereum address embedded into the content (surrounding text doesn't matter).
Copy-paste the posts URL into the above input box and fire away!
diff --git a/cmd/faucet/website.go b/cmd/faucet/website.go index fab1d43460..a091d24919 100644 --- a/cmd/faucet/website.go +++ b/cmd/faucet/website.go @@ -1,12 +1,13 @@ // Code generated by go-bindata. DO NOT EDIT. // sources: -// faucet.html +// faucet.html (11.27kB) package main import ( "bytes" "compress/gzip" + "crypto/sha256" "fmt" "io" "io/ioutil" @@ -19,7 +20,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) + return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer @@ -27,7 +28,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) + return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err @@ -37,8 +38,9 @@ func bindataRead(data []byte, name string) ([]byte, error) { } type asset struct { - bytes []byte - info os.FileInfo + bytes []byte + info os.FileInfo + digest [sha256.Size]byte } type bindataFileInfo struct { @@ -67,7 +69,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x5a\x71\x73\xdb\xb6\x92\xff\xdb\xf9\x14\x5b\x5e\xfc\x24\x9d\x4d\x52\xb6\x93\x3c\x9f\x44\xaa\x93\x97\xd7\xd7\xcb\xcd\x5d\xdb\x69\xd3\xb9\x7b\xd3\xd7\xb9\x01\x89\x95\x88\x18\x04\x58\x00\x94\xac\x7a\xf4\xdd\x6f\x00\x90\x14\x29\xc9\x6e\xd2\xe4\xde\x34\x7f\x38\x24\xb0\xd8\x5d\xec\xfe\x16\xbb\x58\x2a\xf9\xe2\xaf\xdf\xbe\x79\xf7\xf7\xef\xbe\x82\xc2\x94\x7c\xf1\x2c\xb1\xff\x01\x27\x62\x95\x06\x28\x82\xc5\xb3\xb3\xa4\x40\x42\x17\xcf\xce\xce\x92\x12\x0d\x81\xbc\x20\x4a\xa3\x49\x83\xda\x2c\xc3\xdb\x60\x3f\x51\x18\x53\x85\xf8\x4b\xcd\xd6\x69\xf0\x3f\xe1\x8f\xaf\xc3\x37\xb2\xac\x88\x61\x19\xc7\x00\x72\x29\x0c\x0a\x93\x06\x6f\xbf\x4a\x91\xae\xb0\xb7\x4e\x90\x12\xd3\x60\xcd\x70\x53\x49\x65\x7a\xa4\x1b\x46\x4d\x91\x52\x5c\xb3\x1c\x43\xf7\x72\x09\x4c\x30\xc3\x08\x0f\x75\x4e\x38\xa6\x57\xc1\xe2\x99\xe5\x63\x98\xe1\xb8\x78\x78\x88\xbe\x41\xb3\x91\xea\x6e\xb7\x9b\xc1\xeb\xda\x14\x28\x0c\xcb\x89\x41\x0a\x7f\x23\x75\x8e\x26\x89\x3d\xa5\x5b\xc4\x99\xb8\x83\x42\xe1\x32\x0d\xac\xea\x7a\x16\xc7\x39\x15\xef\x75\x94\x73\x59\xd3\x25\x27\x0a\xa3\x5c\x96\x31\x79\x4f\xee\x63\xce\x32\x1d\x9b\x0d\x33\x06\x55\x98\x49\x69\xb4\x51\xa4\x8a\x6f\xa2\x9b\xe8\xcf\x71\xae\x75\xdc\x8d\x45\x25\x13\x51\xae\x75\x00\x0a\x79\x1a\x68\xb3\xe5\xa8\x0b\x44\x13\x40\xbc\xf8\x7d\x72\x97\x52\x98\x90\x6c\x50\xcb\x12\xe3\x17\xd1\x9f\xa3\xa9\x13\xd9\x1f\x7e\x5a\xaa\x15\xab\x73\xc5\x2a\x03\x5a\xe5\x1f\x2c\xf7\xfd\x2f\x35\xaa\x6d\x7c\x13\x5d\x45\x57\xcd\x8b\x93\xf3\x5e\x07\x8b\x24\xf6\x0c\x17\x9f\xc4\x3b\x14\xd2\x6c\xe3\xeb\xe8\x45\x74\x15\x57\x24\xbf\x23\x2b\xa4\xad\x24\x3b\x15\xb5\x83\x9f\x4d\xee\x63\x3e\x7c\x7f\xe8\xc2\xcf\x21\xac\x94\x25\x0a\x13\xbd\xd7\xf1\x75\x74\x75\x1b\x4d\xdb\x81\x63\xfe\x4e\x80\x75\x9a\x15\x75\x16\xad\x51\x59\xe4\xf2\x30\x47\x61\x50\xc1\x83\x1d\x3d\x2b\x99\x08\x0b\x64\xab\xc2\xcc\xe0\x6a\x3a\x3d\x9f\x9f\x1a\x5d\x17\x7e\x98\x32\x5d\x71\xb2\x9d\xc1\x92\xe3\xbd\x1f\x22\x9c\xad\x44\xc8\x0c\x96\x7a\x06\x9e\xb3\x9b\xd8\x39\x99\x95\x92\x2b\x85\x5a\x37\xc2\x2a\xa9\x99\x61\x52\xcc\x2c\xa2\x88\x61\x6b\x3c\x45\xab\x2b\x22\x8e\x16\x90\x4c\x4b\x5e\x1b\x3c\x50\x24\xe3\x32\xbf\xf3\x63\x2e\x9a\xfb\x9b\xc8\x25\x97\x6a\x06\x9b\x82\x35\xcb\xc0\x09\x82\x4a\x61\xc3\x1e\x2a\x42\x29\x13\xab\x19\xbc\xaa\x9a\xfd\x40\x49\xd4\x8a\x89\x19\x4c\xf7\x4b\x92\xb8\x35\x63\x12\xfb\x83\xeb\xd9\x59\x92\x49\xba\x75\x3e\xa4\x6c\x0d\x39\x27\x5a\xa7\xc1\x81\x89\xdd\x81\x34\x20\xb0\xe7\x10\x61\xa2\x9d\x1a\xcc\x29\xb9\x09\xc0\x09\x4a\x03\xaf\x44\x98\x49\x63\x64\x39\x83\x2b\xab\x5e\xb3\xe4\x80\x1f\x0f\xf9\x2a\xbc\xba\x6e\x27\xcf\x92\xe2\xaa\x65\x62\xf0\xde\x84\xce\x3f\x9d\x67\x82\x45\xc2\xda\xb5\x4b\x02\x4b\x12\x66\xc4\x14\x01\x10\xc5\x48\x58\x30\x4a\x51\xa4\x81\x51\x35\x5a\x1c\xb1\x05\xf4\x8f\xbf\x47\x4e\xbf\xe2\xaa\xd5\x2b\xa6\x6c\xdd\x6c\xab\xf7\x78\xb0\xc3\xc7\x37\x71\x0b\xcd\x83\x5c\x2e\x35\x9a\xb0\xb7\xa7\x1e\x31\x13\x55\x6d\xc2\x95\x92\x75\xd5\xcd\x9f\x25\x6e\x14\x18\x4d\x83\x5a\xf1\xa0\x39\xfe\xdd\xa3\xd9\x56\x8d\x29\x82\x6e\xe3\x52\x95\xa1\xf5\x84\x92\x3c\x80\x8a\x93\x1c\x0b\xc9\x29\xaa\x34\xf8\x41\xe6\x8c\x70\x10\x7e\xcf\xf0\xe3\xf7\xff\x09\x8d\xcb\x98\x58\xc1\x56\xd6\x0a\xbe\x32\x05\x2a\xac\x4b\x20\x94\x5a\xb8\x46\x51\xd4\x53\xc4\x61\xf7\x58\xd5\x30\x33\x62\x4f\x75\x96\x64\xb5\x31\xb2\x23\xcc\x8c\x80\xcc\x88\x90\xe2\x92\xd4\xdc\x00\x55\xb2\xa2\x72\x23\x42\x23\x57\x2b\x9b\xe9\xfc\x26\xfc\xa2\x00\x28\x31\xa4\x99\x4a\x83\x96\xb6\xf5\x21\xd1\x95\xac\xea\xaa\xf1\xa2\x1f\xc4\xfb\x8a\x08\x8a\xd4\xfa\x9c\x6b\x0c\x16\x5f\xb3\x35\x42\x89\x7e\x2f\x67\x87\x90\xc8\x89\x42\x13\xf6\x99\x1e\x01\x23\x89\xbd\x32\x7e\x4b\xd0\xfc\x4b\x6a\xde\x72\xea\xb6\x50\xa2\xa8\x61\xf0\x16\x2a\x7b\xae\x04\x8b\x87\x07\x45\xc4\x0a\xe1\x39\xa3\xf7\x97\xf0\x9c\x94\xb2\x16\x06\x66\x29\x44\xaf\xdd\xa3\xde\xed\x06\xdc\x01\x12\xce\x16\x09\x79\x0a\xde\x20\x45\xce\x59\x7e\x97\x06\x86\xa1\x4a\x1f\x1e\x2c\xf3\xdd\x6e\x0e\x0f\x0f\x6c\x09\xcf\xa3\xef\x31\x27\x95\xc9\x0b\xb2\xdb\xad\x54\xfb\x1c\xe1\x3d\xe6\xb5\xc1\xf1\xe4\xe1\x01\xb9\xc6\xdd\x4e\xd7\x59\xc9\xcc\xb8\x5d\x6e\xc7\x05\xdd\xed\xac\xce\x8d\x9e\xbb\x1d\xc4\x96\xa9\xa0\x78\x0f\xcf\xa3\xef\x50\x31\x49\x35\x78\xfa\x24\x26\x8b\x24\xe6\x6c\xd1\xac\x1b\x1a\x29\xae\xf9\x1e\x2f\xb1\x05\x4c\x87\x73\x17\x36\x4e\xd5\xbe\xa6\x27\xa2\x60\x15\x76\xda\x37\x78\xd0\xcc\xe0\x1d\x6e\xd3\xe0\xe1\xa1\xbf\xb6\x99\xcd\x09\xe7\x19\xb1\x76\xf1\x5b\xeb\x16\xfd\x8a\x16\xa7\x6b\xa6\x5d\x49\xb5\x68\x35\xd8\xab\xfd\x81\x61\x7d\x70\x70\x19\x59\xcd\xe0\xe6\xba\x77\x6a\x9d\x8a\xf8\x57\x07\x11\x7f\x73\x92\xb8\x22\x02\x39\xb8\xbf\xa1\x2e\x09\x6f\x9f\x9b\x68\xe9\x05\xdf\xe1\xa2\xd0\x9e\xd1\x9d\x6a\xdd\x59\x3f\x9d\x83\x5c\xa3\x5a\x72\xb9\x99\x01\xa9\x8d\x9c\x43\x49\xee\xbb\x7c\x77\x33\x9d\xf6\xf5\xb6\xa5\x20\xc9\x38\xba\xd3\x45\xe1\x2f\x35\x6a\xa3\xbb\xb3\xc4\x4f\xb9\xbf\xf6\x48\xa1\x28\x34\xd2\x03\x6b\x58\x89\xd6\xb4\x8e\xaa\xe7\xfa\xce\x98\x27\x75\x5f\x4a\xd9\xa5\x90\xbe\x1a\x0d\xeb\x5e\xb6\x0b\x16\x89\x51\x7b\xba\xb3\xc4\xd0\x8f\x4a\x01\xca\x96\x78\x8f\x65\x00\x7f\xa2\xd9\xbd\x57\x88\xca\xd7\x17\x16\xb2\xe0\x5e\x93\xd8\xd0\x4f\x90\x6c\x41\x98\x11\x8d\x1f\x22\xde\x65\xfa\xbd\x78\xf7\xfa\xa9\xf2\x0b\x24\xca\x64\x48\xcc\x87\x28\xb0\xac\x05\xed\xed\xdf\x9d\x9d\x9f\xaa\x40\x2d\xd8\x1a\x95\x66\x66\xfb\xa1\x1a\x20\xdd\xab\xe0\xdf\x87\x2a\x24\xb1\x51\x4f\x63\xad\xff\xf2\x99\x82\xfb\xb7\x4a\x92\x9b\xc5\xbf\xcb\x0d\x50\x89\x1a\x4c\xc1\x34\xd8\xe4\xfa\x65\x12\x17\x37\x1d\x49\xb5\x78\x67\x27\x9c\x51\x61\xe9\x4a\x0b\x60\x1a\x54\x2d\x5c\xe6\x95\x02\x4c\x81\xc3\x72\xa4\x49\xd2\x11\xbc\x93\xb6\xa4\x5b\xa3\x30\x50\x12\xce\x72\x26\x6b\x0d\x24\x37\x52\x69\x58\x2a\x59\x02\xde\x17\xa4\xd6\xc6\x32\xb2\xc7\x07\x59\x13\xc6\x5d\x2c\x39\x97\x82\x54\x40\xf2\xbc\x2e\x6b\x5b\x92\x8a\x15\xa0\x90\xf5\xaa\x68\x74\x31\x12\x7c\x62\xe2\x52\xac\x3a\x7d\x74\x45\x4a\x20\xc6\x90\xfc\x4e\x5f\x42\x7b\x2a\x00\x51\x08\x86\x21\xb5\xab\x72\x59\x96\x52\xc0\x8d\xa2\x50\x11\x65\xb6\xa0\x87\xb5\x05\xc9\x73\x97\xe5\x22\x78\x2d\xb6\x52\x20\x14\x64\xed\x34\x84\x77\xfe\x3a\x71\x09\x5f\x4b\xb9\xe2\x78\x61\x15\xfc\x1b\xc9\x31\x93\xb2\x5b\x06\x25\xd9\xb6\x72\x9b\x6d\x6c\x98\x29\x98\xb7\x53\x85\xaa\xb4\x3c\x28\x70\x56\x32\xa3\xa3\x24\xae\xf6\x47\xeb\x3e\x49\xf3\xb0\x90\x8a\xfd\x6a\x2b\x1c\xde\x3f\x47\xcd\xc1\x29\xd3\x1e\x92\xce\xfd\x1c\x97\x66\x06\x2f\xfc\x21\x79\x08\xe8\xe6\x2a\x74\x0a\xcd\x2d\x4f\x77\xc5\xb4\x99\x67\x06\x37\xbe\xae\xf5\x15\x05\x35\x3d\x0d\xe8\x01\xe6\xbc\xd0\xdb\xdb\xea\xbe\xd3\xa3\x2b\x8e\xa7\x1d\x13\x0b\x85\xa1\x51\xd6\xac\x67\xcf\x92\xdc\x21\x10\x48\xc8\xc1\x55\xb9\x51\xda\x5d\xb4\x98\x6b\x14\xc4\x66\x83\x68\xbe\xb4\x31\x9c\x7e\xef\x19\x32\xb1\x3a\xbf\x9e\x7a\x68\xda\x07\xcb\xfe\xfc\x7a\xca\x84\x91\xe7\xd7\xd3\xe9\xfd\xf4\x03\xff\x9d\x5f\x4f\xa5\x38\xbf\x9e\x9a\x02\xcf\xaf\xa7\xe7\xd7\x37\x7d\x50\xfb\x91\xb6\xc4\xb4\x54\xa8\xad\xb4\x16\xeb\x01\x18\xa2\x56\x68\xd2\xe0\x7f\x49\x26\x6b\x33\xcb\x38\x11\x77\xc1\xc2\xa9\x6b\xcb\x0e\x87\x82\xd3\x85\x2a\x54\x44\x5b\x48\x58\x8d\x1d\x4a\x9a\xa6\x88\x86\xb1\xae\x95\x92\xb5\xb0\xe9\x11\xec\x9e\x5d\xa8\x8a\x91\x45\x99\x35\xcc\x24\x4a\x32\x15\x2f\xde\xc8\x6a\x1b\x3a\x26\x6e\xf9\x91\x19\x75\x5d\x55\x52\x99\xa8\x6f\x4e\x62\x2f\x44\x1c\x75\x7c\x3b\x7d\x79\xfb\xea\x49\xf5\xb5\x2d\xb7\xdd\x1e\x3a\x0d\x49\x26\xd7\x08\xbe\xb8\xcf\xe4\x3d\x10\x41\x61\xc9\x14\x02\xd9\x90\xed\x17\x49\x4c\xdd\x55\xec\xd3\x51\xbb\x72\x81\x16\x56\xbc\xd6\xb6\x16\x61\x36\x50\xff\x50\x10\xf6\x27\x01\x7c\xc7\x6b\x7d\x09\x55\x9d\x71\xa6\x0b\x20\x20\x70\x03\x89\x36\x4a\x8a\xd5\xc2\x8d\xe6\xf6\xaa\xea\x5e\xa1\x92\xda\x3c\x85\x06\x2c\x33\xa4\xf4\x04\x1e\x7e\x27\x1c\xac\x3c\xe7\xc2\x7f\xbe\xfb\x96\xcd\xe1\xf8\x87\x72\x59\x7b\x62\xff\x51\xfd\x75\x14\xbe\x9b\xcd\x26\x6a\x2d\xe9\x62\xb7\x40\x5e\xc5\x36\x8d\xd5\x82\x99\x6d\xec\x4f\x41\x29\xe2\x2f\x19\x4d\xaf\x6f\xaf\x5f\xbd\xba\x7e\xf1\x6f\xb7\x2f\x5f\x5e\xdf\xbe\x78\xf9\x58\x60\x77\xa0\xf8\xfd\x71\xed\xaf\x43\xdf\xc8\xd7\xb5\x29\xba\xbb\x90\xc7\x4b\x5b\x83\xdb\x4a\x8b\xda\xbb\xa4\x0a\x7e\x37\x86\x6a\x61\x0b\xca\x90\xf0\x93\xb5\xe0\x47\xa0\xc8\xc1\xe8\x09\xcd\x3e\x11\x5a\x2d\x7c\x2c\x52\x64\x6d\xec\x0e\xdb\xa6\x0c\x93\xa2\x83\xd3\x25\x68\x56\x56\x7c\x0b\xf9\xde\xeb\xa7\x71\xf5\xa8\x53\x7e\x13\x56\x43\xb7\x79\x90\xb9\x2a\xae\x94\x14\x6d\xf5\xa6\x6b\x9d\x63\xe5\xba\xf5\xb6\x22\xfa\xcb\xf6\x57\x22\x0c\x13\xd8\x56\x4e\x11\x7c\x2b\xf8\x16\x6a\x8d\xb0\x94\x0a\x28\x66\xf5\x6a\xe5\xca\x3d\x05\x95\x62\x6b\x62\xb0\x2d\x97\x74\x83\x8a\x0e\x14\xbd\x1b\xaa\x2d\x5d\x79\xaf\x92\xfc\xbb\xac\x21\x27\x02\x8c\x22\xf9\x9d\x8f\x94\x5a\x29\x1b\x29\x15\xfa\xdd\x74\x05\x5b\x86\x5c\x6e\x1c\x89\xdf\xf7\x92\x21\x77\xd5\x9b\x46\x84\x42\x6e\xa0\xac\x73\x17\x90\xb6\x3a\x73\x9b\xd8\x10\x66\xa0\x16\x86\x71\x6f\x4f\x53\x2b\x61\x6b\x3d\x1c\x14\x59\x47\x77\xf8\x04\xcb\xc5\xbb\x02\x4f\x94\xb6\xdd\xed\x1b\x14\xbe\xf1\xe4\x50\x29\x69\x30\xb7\x0e\x05\xb2\x22\x4c\x68\xeb\x11\x57\xc6\x61\xf9\x01\xb7\xf3\xee\xa9\x79\xd8\x77\x9a\xdd\x74\x1c\xc3\xd7\x5c\x66\x84\xc3\xda\x22\x3d\xe3\xb6\x2c\x97\x50\x48\xbb\xf5\x9e\xb5\xb4\x21\xa6\xd6\x20\x97\x6e\xd4\x6b\x6e\xd7\xaf\x89\xb2\x1e\xc4\xb2\x32\x90\x36\x7d\x52\x3b\xa6\x51\xad\x9b\xee\xaf\x7d\x35\x0c\xd5\x60\xbe\xb3\x7a\x0a\x3f\xfd\x3c\x7f\xd6\xa8\xf2\x57\x5c\x3a\x48\x58\x7c\xfb\x2d\x9b\x82\x18\xc8\x15\x12\x83\x1a\x72\x2e\x75\xad\xbc\x86\x54\xc9\x0a\xac\x96\x2d\xa7\x96\xb3\x9d\xa8\x9c\xb4\x96\xc9\xb8\x20\xba\x98\x34\x6d\x5e\x85\xce\x4b\xdd\x5c\x3b\x7e\x66\x51\x37\xb6\x0c\x58\x3a\x9d\x03\x4b\x5a\xbe\x11\x47\xb1\x32\xc5\x1c\xd8\xc5\x45\x47\x7c\xc6\x96\x30\x6e\x29\x7e\x62\x3f\x47\xe6\x3e\xb2\x52\x20\x4d\xa1\x2f\xcd\x09\x6c\xf8\xe8\x8a\xb3\x1c\xc7\xec\x12\xae\x26\xf3\x76\x36\x53\x48\xee\xda\xb7\xc6\x8f\xfe\x3f\xf7\x77\x37\x1f\x5a\xc6\x19\x7f\x60\x1b\xdf\xc3\xd1\x40\x60\xc5\xb4\x81\x5a\x71\x68\x62\xd8\xbb\xa0\x73\x88\xa3\xeb\x5b\xe5\x08\x97\xcd\x43\x83\xa9\x76\x0b\x9e\x4d\xa4\x51\xd0\xf1\x7f\xfc\xf0\xed\x37\x91\x36\x8a\x89\x15\x5b\x6e\xc7\x0f\xb5\xe2\x33\x78\x3e\x0e\xfe\xa5\x56\x3c\x98\xfc\x34\xfd\x39\x5a\x13\x5e\xe3\xa5\xf3\xf7\xcc\xfd\x3d\x92\x72\x09\xcd\xe3\x0c\x86\x02\x77\x93\xc9\xfc\x74\xbf\xab\xd7\x9e\x53\xa8\xd1\x8c\x2d\x61\x07\xfc\x43\x1b\x11\x28\xd1\x14\xd2\x85\xae\xc2\x5c\x0a\x81\xb9\x81\xba\x92\xa2\x31\x09\x70\xa9\xf5\x1e\x88\x2d\x45\x7a\x0c\x8a\x86\x3e\x75\xc9\xfa\xbf\x31\xfb\x41\xe6\x77\x68\xc6\xe3\xf1\x86\x09\x2a\x37\x11\x97\xfe\xa8\x8d\x6c\x90\xca\x5c\x72\x48\xd3\x14\x9a\x2c\x1a\x4c\xe0\x4b\x08\x36\xda\xe6\xd3\x00\x66\xf6\xd1\x3e\x4d\xe0\x02\x0e\x97\x17\x36\xdf\x5f\x40\x10\x93\x8a\x05\x13\x1f\x0e\xad\xe1\xa5\x28\x51\x6b\xb2\xc2\xbe\x82\xee\x86\xdb\x81\xcc\xee\xa3\xd4\x2b\x48\xc1\x39\xa8\x22\x4a\xa3\x27\x89\x28\x31\xa4\x45\x9b\xc5\xac\x23\x4b\x53\x10\x35\xe7\x7b\x90\xfa\xa0\x98\xb7\xf0\x1b\x90\x47\x3e\xd7\x7c\x91\xa6\x50\x0b\xea\x4c\x4c\xf7\x2b\xad\xf3\x7d\x33\x64\x12\xd9\xbc\xb0\x5f\x31\x99\xf7\xd1\x3c\xe0\x86\xf4\xb7\xd8\x21\x3d\xe4\x87\xf4\x11\x86\xae\xf7\xf4\x14\x3f\xdf\xab\xea\xb1\x73\x03\x8f\x70\x13\x75\x99\xa1\x7a\x8a\x9d\xef\x3d\x35\xec\x9c\xa9\xdf\x0a\xd3\x5b\x7b\x09\x57\xaf\x26\x8f\x70\x47\xa5\xe4\xa3\xcc\x85\x34\xdb\xf1\x03\x27\x5b\x5b\x33\xc1\xc8\xc8\xea\x8d\x6b\x15\x8d\x2e\x5d\xc6\x9d\x41\xc7\xe1\xd2\x7d\x04\x98\xc1\xc8\xbd\xd9\x79\x56\xa2\x5b\xf5\x72\x3a\x9d\x5e\x42\xfb\xf5\xec\x2f\xc4\x06\xa1\xaa\x71\xf7\x88\x3e\xba\xce\x73\x9b\xf7\x3f\x45\xa3\x86\x47\xa7\x53\xf3\xfe\x09\x5a\x75\xb9\x61\xa0\x16\xfc\xe9\x4f\x70\x34\x3b\x84\x71\x1c\xc3\x7f\x11\x75\xe7\x1a\x3b\x95\xc2\xb5\x6b\xfe\x74\xf4\x25\xd3\xda\x35\x55\x34\x50\x29\xb0\x59\xf3\x71\xc7\xfe\x91\x8e\x0d\x19\x2c\x60\x7a\xa8\xa0\x3d\x0e\x7b\x69\xe1\x44\xb6\xe8\xf1\x1d\x26\x82\xb3\x5d\x5f\xde\x60\x25\x2b\x11\xbe\x48\x21\x08\xfa\x8b\x8f\x28\x2c\x41\xc7\xec\x4c\xa3\x79\xe7\x7d\x31\x6e\xb2\xe3\xa9\xdc\x35\xb9\x84\x9b\xe9\x74\x3a\x39\x52\x62\xb7\x37\xef\xeb\xca\x96\x4d\x40\xc4\xd6\x1d\x89\x9d\x6d\x5d\xe1\x68\x4b\x20\x7b\xa4\x71\xc8\x25\xe7\xbe\x66\x69\x96\x5a\x03\x37\x4d\xb0\x14\xc2\xab\xf9\x89\x2c\xda\xb3\x64\x6f\x6b\x87\xee\x39\x61\xfb\x43\x17\x0d\x6d\x76\x40\x1c\x5e\x0d\x9c\x32\xf0\xd7\x69\xc7\x9c\x75\x7a\xb3\xbd\x45\x0f\xdc\xb5\xf7\xd7\xa1\xcd\x7a\xfa\x7b\x3e\x17\x57\x1f\xb8\x8d\x6e\xba\xaa\x75\x31\x3e\x50\x74\x32\x3f\xf6\xcd\x5b\x83\xca\x56\xc9\xd2\xa6\x2c\xeb\x0b\x7b\x15\x50\x78\xe4\x12\x57\xaa\x2b\x0c\x15\x0a\x8a\xaa\x2d\x29\x7c\x65\x6f\x0b\xc0\x81\xcb\xfc\xad\xb2\x0f\xa7\x8f\x0c\x18\x57\x92\x49\x81\x00\x00\x07\x41\xe0\x80\x3a\x40\xaa\x25\x46\x4e\x2a\x8d\x14\x52\xf0\x3f\x66\x18\x4f\xa2\x5a\xb0\xfb\xf1\x24\x6c\xde\x0f\x79\xb4\xf3\xf3\xee\x9a\xd8\xaa\x7d\x91\x42\x90\x18\x05\x8c\xa6\xa3\x00\x2e\x4e\x85\xa0\xcd\xba\xa3\xc5\x5e\x83\xfe\x52\x80\xc4\xd0\x85\xeb\x67\xfb\xfb\xda\x3f\x82\x8c\xe4\x77\x2b\x77\x11\x9a\xd9\x52\x6b\x7c\xc4\x96\xac\x89\x21\xca\x71\x9d\xcc\x61\x4f\xde\x5c\x14\x73\xeb\x9c\x39\xf8\x1b\xa9\x6b\x9b\x43\xf7\xa9\xc9\xbd\x65\x52\x51\x54\xa1\x22\x94\xd5\x7a\x06\x2f\xaa\xfb\xf9\x3f\xda\x4f\x71\xae\xb9\xff\xa4\xaa\x95\xc2\xc5\x91\x46\x4d\x93\xf8\x02\x82\x24\xb6\x04\xbf\xc5\xa6\xdb\x6c\xff\x47\x14\x70\xe2\x13\x06\x74\x3f\x71\x68\xc6\x4b\x46\x29\x47\xab\xf0\x9e\xbd\x0d\x46\xeb\xff\x7e\x48\x0d\x45\x42\xf3\xed\x62\xbf\x66\x07\xc8\x35\x3e\xb1\xa0\xfb\x0c\x32\xb2\x00\x08\xed\x96\x99\xb3\x79\x73\xd9\x76\xc3\x6a\xe4\x6c\xd1\xfc\x24\x86\xd6\xca\xd5\x5a\xe3\xb0\x01\xd8\x25\x8c\xb4\xad\xfd\xa8\x1e\x4d\xa2\xa2\x2e\x89\x60\xbf\xe2\xd8\xe6\xa5\x89\xb7\x95\xfb\xae\x12\x1c\x1f\xc9\x47\xca\xec\x3f\x78\x8c\xda\x1c\x37\x6a\x8c\x38\x6a\xbd\xfb\x62\x7f\xb7\x9f\xc1\x74\x3e\xfa\x48\x0b\x9d\x96\x12\x66\x44\x41\xff\x25\x6c\x93\x2f\x28\x69\xa5\xb7\x73\x19\x51\x23\xdf\xc9\x70\xf5\xb9\x90\x9b\x74\x74\x33\xed\x94\xf4\x8e\x76\x7e\x1e\x35\x58\x3b\x72\x86\xd5\xb2\x0d\xcd\x05\xdc\x4c\x3f\x87\xb6\xbe\x1b\x72\xb0\x03\xa3\x58\x85\x14\x48\x6e\xd8\x1a\xff\x1f\x36\xf2\x19\x8c\xfc\xd1\x2a\x5a\x1c\xb6\xc6\x73\x30\x1d\xe8\x6b\x67\x3b\xdb\xfe\xab\x8d\x37\x88\x9d\x85\x2f\x20\x38\xb9\x91\x47\x91\x78\x40\x78\x10\xda\x8f\xc7\xbd\xfb\x50\x18\x1c\xe6\x14\x5b\xed\x76\x1f\xb9\x27\x51\x61\x4a\x3e\x0e\x12\xe3\x7e\xec\x64\x75\xee\x38\x38\x06\x7e\x78\x58\xd2\xed\x86\x17\x19\x7b\x7f\xc7\x83\x7b\x16\xf4\x8a\x93\xee\x2e\xd6\x56\x22\xb0\xdb\xff\x26\x2c\x8e\xe1\x07\x43\x94\x01\x02\x3f\xbe\x85\xba\xa2\xc4\xf8\x4f\x72\x36\x3f\xfa\xae\x73\xfb\xa3\xb1\x8c\x28\x0d\x4b\xa9\x36\x44\xd1\xa6\x3f\x63\x0a\xdc\xba\x4f\x72\x6d\xe9\xa7\xd1\xbc\xb5\xa7\xd8\x9a\xf0\xf1\xd1\xbd\xef\xf9\x78\x14\xf5\x5d\x3e\x9a\x44\x48\xf2\xe2\x98\xd0\x65\xac\x4e\x6e\x0a\xdf\xb8\x2b\xc0\xf8\xf9\xd8\x14\x4c\x4f\x22\x62\x8c\x1a\x8f\x06\x60\x18\x4d\xac\x5f\xaf\x7a\x57\xb2\x6e\x79\x32\x08\xab\xa7\x78\xec\x8b\xe9\xae\x10\x68\xc9\x73\xad\xc7\x1e\x57\xa3\xcb\x1e\xef\x21\xac\x46\xe7\xa3\xce\x51\xfb\xf0\xde\xef\x23\x3d\xa9\xc9\x80\xf5\xc8\x46\xd9\xe8\x48\x3c\xa1\xf4\x8d\x8d\x9f\x71\x70\x22\xd2\x0f\xd1\x31\xe9\x8c\xed\xcf\xeb\x27\xad\xec\x7f\x5e\xf3\x88\x89\x19\x1d\x4d\x22\x5d\x67\xbe\x37\x31\x7e\xd9\x5d\xc0\x5a\x32\x07\xde\xc3\x54\x70\x54\x50\x58\x11\xc3\xa2\x22\x3c\x28\x42\x9e\xc8\x1a\x8d\x48\xbf\xab\xdd\xa5\x35\xf8\x74\xd2\xb5\xb6\xbe\xd2\xb6\xb8\xf2\xad\xff\x0d\x66\xda\x75\x12\xa0\xc1\xbb\xeb\xe6\xf8\xae\xcd\xeb\xef\xde\xf6\x3a\x37\x5d\x44\x8c\x1d\xf7\xee\xf7\x9c\xa7\xfa\x24\x27\x7f\x40\xba\xd9\x6c\x22\xff\x45\xcb\xb5\xf1\xbb\x46\x4a\x4c\x2a\x16\xbd\xd7\x01\x10\xbd\x15\x39\x50\x5c\xa2\x5a\xf4\xd8\x37\xdd\x95\x24\xf6\x3f\x6d\x4c\x62\xff\xeb\xed\xff\x0b\x00\x00\xff\xff\x56\xf8\xb5\xef\xce\x2d\x00\x00") +var _faucetHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x5a\x6d\x93\xdb\x36\x92\xfe\x3c\xfe\x15\x1d\x9e\xbd\x92\xce\x43\x52\x33\x63\x7b\x7d\x12\xa9\x94\xd7\x9b\xdd\xf3\xd5\x5d\x92\x4a\x9c\xba\xdb\xca\xa6\xae\x40\xb2\x25\xc2\x03\x02\x0c\x00\x4a\xa3\x4c\xe9\xbf\x5f\x35\x40\x52\xd4\xcb\x4c\xec\xb5\xaf\x6a\xfd\x61\x4c\x02\x8d\x46\xa3\xfb\x69\xf4\x0b\x95\x7c\xf5\xe7\xef\xde\xbe\xff\xdb\xf7\xdf\x40\x69\x2b\xb1\x78\x92\xd0\x7f\x20\x98\x5c\xa5\x01\xca\x60\xf1\xe4\x22\x29\x91\x15\x8b\x27\x17\x17\x49\x85\x96\x41\x5e\x32\x6d\xd0\xa6\x41\x63\x97\xe1\xeb\x60\x3f\x51\x5a\x5b\x87\xf8\x6b\xc3\xd7\x69\xf0\x3f\xe1\x4f\x6f\xc2\xb7\xaa\xaa\x99\xe5\x99\xc0\x00\x72\x25\x2d\x4a\x9b\x06\xef\xbe\x49\xb1\x58\xe1\x60\x9d\x64\x15\xa6\xc1\x9a\xe3\xa6\x56\xda\x0e\x48\x37\xbc\xb0\x65\x5a\xe0\x9a\xe7\x18\xba\x97\x4b\xe0\x92\x5b\xce\x44\x68\x72\x26\x30\xbd\x0a\x16\x4f\x88\x8f\xe5\x56\xe0\xe2\xfe\x3e\xfa\x16\xed\x46\xe9\xdb\xdd\x6e\x06\x6f\x1a\x5b\xa2\xb4\x3c\x67\x16\x0b\xf8\x0b\x6b\x72\xb4\x49\xec\x29\xdd\x22\xc1\xe5\x2d\x94\x1a\x97\x69\x40\xa2\x9b\x59\x1c\xe7\x85\xfc\x60\xa2\x5c\xa8\xa6\x58\x0a\xa6\x31\xca\x55\x15\xb3\x0f\xec\x2e\x16\x3c\x33\xb1\xdd\x70\x6b\x51\x87\x99\x52\xd6\x58\xcd\xea\xf8\x26\xba\x89\xfe\x18\xe7\xc6\xc4\xfd\x58\x54\x71\x19\xe5\xc6\x04\xa0\x51\xa4\x81\xb1\x5b\x81\xa6\x44\xb4\x01\xc4\x8b\x7f\x6c\xdf\xa5\x92\x36\x64\x1b\x34\xaa\xc2\xf8\x45\xf4\xc7\x68\xea\xb6\x1c\x0e\x3f\xbe\x2b\x6d\x6b\x72\xcd\x6b\x0b\x46\xe7\x1f\xbd\xef\x87\x5f\x1b\xd4\xdb\xf8\x26\xba\x8a\xae\xda\x17\xb7\xcf\x07\x13\x2c\x92\xd8\x33\x5c\x7c\x16\xef\x50\x2a\xbb\x8d\xaf\xa3\x17\xd1\x55\x5c\xb3\xfc\x96\xad\xb0\xe8\x76\xa2\xa9\xa8\x1b\xfc\x62\xfb\x3e\x64\xc3\x0f\xc7\x26\xfc\x12\x9b\x55\xaa\x42\x69\xa3\x0f\x26\xbe\x8e\xae\x5e\x47\xd3\x6e\xe0\x94\xbf\xdb\x80\x8c\x46\x5b\x5d\x44\x6b\xd4\x84\x5c\x11\xe6\x28\x2d\x6a\xb8\xa7\xd1\x8b\x8a\xcb\xb0\x44\xbe\x2a\xed\x0c\xae\xa6\xd3\x67\xf3\x73\xa3\xeb\xd2\x0f\x17\xdc\xd4\x82\x6d\x67\xb0\x14\x78\xe7\x87\x98\xe0\x2b\x19\x72\x8b\x95\x99\x81\xe7\xec\x26\x76\x6e\xcf\x5a\xab\x95\x46\x63\xda\xcd\x6a\x65\xb8\xe5\x4a\xce\x08\x51\xcc\xf2\x35\x9e\xa3\x35\x35\x93\x27\x0b\x58\x66\x94\x68\x2c\x1e\x09\x92\x09\x95\xdf\xfa\x31\xe7\xcd\xc3\x43\xe4\x4a\x28\x3d\x83\x4d\xc9\xdb\x65\xe0\x36\x82\x5a\x63\xcb\x1e\x6a\x56\x14\x5c\xae\x66\xf0\xaa\x6e\xcf\x03\x15\xd3\x2b\x2e\x67\x30\xdd\x2f\x49\xe2\x4e\x8d\x49\xec\x2f\xae\x27\x17\x49\xa6\x8a\xad\xb3\x61\xc1\xd7\x90\x0b\x66\x4c\x1a\x1c\xa9\xd8\x5d\x48\x07\x04\x74\x0f\x31\x2e\xbb\xa9\x83\x39\xad\x36\x01\xb8\x8d\xd2\xc0\x0b\x11\x66\xca\x5a\x55\xcd\xe0\x8a\xc4\x6b\x97\x1c\xf1\x13\xa1\x58\x85\x57\xd7\xdd\xe4\x45\x52\x5e\x75\x4c\x2c\xde\xd9\xd0\xd9\xa7\xb7\x4c\xb0\x48\x78\xb7\x76\xc9\x60\xc9\xc2\x8c\xd9\x32\x00\xa6\x39\x0b\x4b\x5e\x14\x28\xd3\xc0\xea\x06\x09\x47\x7c\x01\xc3\xeb\xef\x81\xdb\xaf\xbc\xea\xe4\x8a\x0b\xbe\x6e\x8f\x35\x78\x3c\x3a\xe1\xc3\x87\x78\x0d\xed\x83\x5a\x2e\x0d\xda\x70\x70\xa6\x01\x31\x97\x75\x63\xc3\x95\x56\x4d\xdd\xcf\x5f\x24\x6e\x14\x78\x91\x06\x8d\x16\x41\x7b\xfd\xbb\x47\xbb\xad\x5b\x55\x04\xfd\xc1\x95\xae\x42\xb2\x84\x56\x22\x80\x5a\xb0\x1c\x4b\x25\x0a\xd4\x69\xf0\xa3\xca\x39\x13\x20\xfd\x99\xe1\xa7\x1f\xfe\x13\x5a\x93\x71\xb9\x82\xad\x6a\x34\x7c\x63\x4b\xd4\xd8\x54\xc0\x8a\x82\xe0\x1a\x45\xd1\x40\x10\x87\xdd\x53\x51\xc3\xcc\xca\x3d\xd5\x45\x92\x35\xd6\xaa\x9e\x30\xb3\x12\x32\x2b\xc3\x02\x97\xac\x11\x16\x0a\xad\xea\x42\x6d\x64\x68\xd5\x6a\x45\x91\xce\x1f\xc2\x2f\x0a\xa0\x60\x96\xb5\x53\x69\xd0\xd1\x76\x36\x64\xa6\x56\x75\x53\xb7\x56\xf4\x83\x78\x57\x33\x59\x60\x41\x36\x17\x06\x83\xc5\x5f\xf9\x1a\xa1\x42\x7f\x96\x8b\x63\x48\xe4\x4c\xa3\x0d\x87\x4c\x4f\x80\x91\xc4\x5e\x18\x7f\x24\x68\xff\x25\x8d\xe8\x38\xf5\x47\xa8\x50\x36\x70\xf0\x16\x6a\xba\x57\x82\xc5\xfd\xbd\x66\x72\x85\xf0\x94\x17\x77\x97\xf0\x94\x55\xaa\x91\x16\x66\x29\x44\x6f\xdc\xa3\xd9\xed\x0e\xb8\x03\x24\x82\x2f\x12\xf6\x18\xbc\x41\xc9\x5c\xf0\xfc\x36\x0d\x2c\x47\x9d\xde\xdf\x13\xf3\xdd\x6e\x0e\xf7\xf7\x7c\x09\x4f\xa3\x1f\x30\x67\xb5\xcd\x4b\xb6\xdb\xad\x74\xf7\x1c\xe1\x1d\xe6\x8d\xc5\xf1\xe4\xfe\x1e\x85\xc1\xdd\xce\x34\x59\xc5\xed\xb8\x5b\x4e\xe3\xb2\xd8\xed\x48\xe6\x56\xce\xdd\x0e\x62\x62\x2a\x0b\xbc\x83\xa7\xd1\xf7\xa8\xb9\x2a\x0c\x78\xfa\x24\x66\x8b\x24\x16\x7c\xd1\xae\x3b\x54\x52\xdc\x88\x3d\x5e\x62\x02\x4c\x8f\x73\xe7\x36\x4e\xd4\xa1\xa4\x67\xbc\x60\x15\xf6\xd2\xb7\x78\x30\xdc\xe2\x2d\x6e\xd3\xe0\xfe\x7e\xb8\xb6\x9d\xcd\x99\x10\x19\x23\xbd\xf8\xa3\xf5\x8b\x7e\x43\xc2\xe9\x9a\x1b\x97\x52\x2d\x3a\x09\xf6\x62\x7f\xa4\x5b\x1f\x5d\x5c\x56\xd5\x33\xb8\xb9\x1e\xdc\x5a\xe7\x3c\xfe\xd5\x91\xc7\xdf\x9c\x25\xae\x99\x44\x01\xee\x6f\x68\x2a\x26\xba\xe7\xd6\x5b\x06\xce\x77\xbc\x28\xa4\x3b\xba\x17\xad\xbf\xeb\xa7\x73\x50\x6b\xd4\x4b\xa1\x36\x33\x60\x8d\x55\x73\xa8\xd8\x5d\x1f\xef\x6e\xa6\xd3\xa1\xdc\x94\x0a\xb2\x4c\xa0\xbb\x5d\x34\xfe\xda\xa0\xb1\xa6\xbf\x4b\xfc\x94\xfb\x4b\x57\x4a\x81\xd2\x60\x71\xa4\x0d\xda\x91\x54\xeb\xa8\x06\xa6\xef\x95\x79\x56\xf6\xa5\x52\x7d\x08\x19\x8a\xd1\xb2\x1e\x44\xbb\x60\x91\x58\xbd\xa7\xbb\x48\x6c\xf1\x49\x21\x40\x53\x8a\xf7\x50\x04\xf0\x37\x1a\x9d\xbd\x46\xd4\x3e\xbf\x20\xc8\x82\x7b\x4d\x62\x5b\x7c\xc6\xce\x04\xc2\x8c\x19\xfc\x98\xed\x5d\xa4\xdf\x6f\xef\x5e\x3f\x77\xff\x12\x99\xb6\x19\x32\xfb\x31\x02\x2c\x1b\x59\x0c\xce\xef\xee\xce\xcf\x15\xa0\x91\x7c\x8d\xda\x70\xbb\xfd\x58\x09\xb0\xd8\x8b\xe0\xdf\x0f\x45\x48\x62\xab\x1f\xc7\xda\xf0\xe5\x0b\x39\xf7\xef\xa5\x24\x37\x8b\x7f\x57\x1b\x28\x14\x1a\xb0\x25\x37\x40\xc1\xf5\xeb\x24\x2e\x6f\x7a\x92\x7a\xf1\x9e\x26\x9c\x52\x61\xe9\x52\x0b\xe0\x06\x74\x23\x5d\xe4\x55\x12\x6c\x89\x87\xe9\x48\x1b\xa4\x23\x78\xaf\x28\xa5\x5b\xa3\xb4\x50\x31\xc1\x73\xae\x1a\x03\x2c\xb7\x4a\x1b\x58\x6a\x55\x01\xde\x95\xac\x31\x96\x18\xd1\xf5\xc1\xd6\x8c\x0b\xe7\x4b\xce\xa4\xa0\x34\xb0\x3c\x6f\xaa\x86\x52\x52\xb9\x02\x94\xaa\x59\x95\xad\x2c\x56\x81\x0f\x4c\x42\xc9\x55\x2f\x8f\xa9\x59\x05\xcc\x5a\x96\xdf\x9a\x4b\xe8\x6e\x05\x60\x1a\xc1\x72\x2c\x68\x55\xae\xaa\x4a\x49\xb8\xd1\x05\xd4\x4c\xdb\x2d\x98\xc3\xdc\x82\xe5\xb9\x8b\x72\x11\xbc\x91\x5b\x25\x11\x4a\xb6\x76\x12\xc2\x7b\x5f\x4e\x90\x5c\x7f\x61\x39\x66\x4a\xf5\xd4\x50\xb1\x6d\xb7\x5d\x2b\xfd\x86\xdb\x92\x7b\xf5\xd4\xa8\x2b\x5a\x5a\x80\xe0\x15\xb7\x26\x4a\xe2\x7a\x7f\xa3\xee\x63\xb3\x08\x4b\xa5\xf9\x6f\x94\xd8\x88\xe1\xf5\x69\x8f\x2e\x97\xee\x6e\x74\x56\x17\xb8\xb4\x33\x78\xe1\xef\xc6\x63\x1c\xb7\x15\xd0\x39\x10\x77\x3c\x5d\x65\x49\x01\x67\x06\x37\x3e\x9d\xf5\x89\x44\x61\x07\x12\x14\x47\x50\xf3\x9b\xbe\x7e\x5d\xdf\xf5\x72\xf4\x39\xf1\xb4\x67\x42\x08\x38\x54\xca\x9a\xf7\x6a\xbc\x84\x8a\xdd\x22\x30\x48\xd8\x51\x85\xdc\x0a\xed\xea\x2b\xee\xfa\x03\xb1\xdd\x20\xda\xaf\xc9\x75\xd3\x1f\x3c\x43\x2e\x57\xcf\xae\xa7\x1e\x91\xf4\x40\xec\x9f\x5d\x4f\xb9\xb4\xea\xd9\xf5\x74\x7a\x37\xfd\xc8\x7f\xcf\xae\xa7\x4a\x3e\xbb\x9e\xda\x12\x9f\x5d\x4f\x9f\x5d\xdf\x0c\xb1\xec\x47\xba\xcc\x92\xa8\xd0\xd0\x6e\x1d\xc4\x03\xb0\x4c\xaf\xd0\xa6\xc1\xff\xb2\x4c\x35\x76\x96\x09\x26\x6f\x83\x85\x13\x97\xb2\x0d\x87\x82\xf3\xf9\x29\xd4\xcc\x10\x24\x48\x62\x87\x92\xb6\x17\x62\x60\x6c\x1a\xad\x55\x23\x29\x2a\x02\x9d\xd9\x79\xa8\x1c\x11\xca\x48\x31\x93\x28\xc9\x74\xbc\x78\xab\xea\x6d\xe8\x98\xb8\xe5\x27\x6a\x34\x4d\x5d\x2b\x6d\xa3\xa1\x3a\x19\xd5\x41\x02\x4d\xfc\x7a\xfa\xf2\xf5\xab\x47\xc5\x37\x94\x65\xbb\x33\xf4\x12\xb2\x4c\xad\x11\x7c\x4e\x9f\xa9\x3b\x60\xb2\x80\x25\xd7\x08\x6c\xc3\xb6\x5f\x25\x71\xe1\x2a\xb0\xcf\x47\xed\xb2\xf5\xae\x7f\x2a\xd8\x76\x2e\x7f\x09\x75\x93\x09\x6e\x4a\x60\x20\x71\x03\x89\xb1\x5a\xc9\xd5\xc2\x8d\xe6\x54\x92\xba\x57\xa8\x95\xb1\x8f\x99\x1f\xab\x0c\x8b\xe2\x0c\x00\xbe\x94\xfd\x37\x9b\x4d\xd4\x69\xd2\x19\xbf\x44\x51\xc7\x74\xfd\x35\x92\xdb\x6d\xec\xdd\x48\xc9\xf8\x6b\x5e\xa4\xd7\xaf\xaf\x5f\xbd\xba\x7e\xf1\x6f\xaf\x5f\xbe\xbc\x7e\xfd\xe2\xe5\x43\xc8\xa0\x43\x7d\x26\x30\x7c\x1a\xfd\xad\xa2\xaa\xb5\xcf\xa1\x3d\x5e\xba\xdc\x8d\x22\x74\x41\x35\x88\x0e\xfe\x61\x0c\x35\x92\x12\x91\x90\x89\xb3\x39\xc4\x27\xa0\xc8\xc1\xe8\x11\xc9\x3e\x13\x5a\x1d\x7c\x08\x29\xaa\xb1\x74\xc2\xae\x98\xe7\x4a\xf6\x70\xba\x04\xc3\xab\x5a\x6c\x21\xdf\x5b\xfd\x3c\xae\x1e\x34\xca\xef\xc2\xea\xd0\x6c\x1e\x64\x2e\xfa\x57\xaa\x40\x8a\xfa\xa6\x31\x39\xd6\xae\xcb\x4b\x91\xf4\x4f\xdb\xdf\x98\xb4\x5c\x62\x17\x71\x23\xf8\x4e\x8a\x2d\x34\x06\x61\xa9\x34\x14\x98\x35\xab\x95\x4b\x13\x34\xd4\x9a\xaf\x99\xc5\x2e\xcc\x9a\x16\x15\x3d\x28\x06\x95\x0d\xa5\x3c\x62\x90\x81\xfc\x4d\x35\x90\x33\x09\x56\xb3\xfc\xd6\x7b\x4a\xa3\x35\x79\x4a\x8d\xfe\x34\x7d\xa0\xcf\x50\xa8\x8d\x23\xf1\xe7\x5e\x72\x14\x2e\xea\x1b\x44\x28\xd5\x06\xaa\x26\x77\x0e\x49\x51\xdd\x1d\x62\xc3\xb8\x85\x46\x5a\x2e\xbc\x3e\x6d\xa3\x25\xe5\x08\x78\x10\xa5\x4f\x6a\xbf\x04\xab\xc5\xfb\x12\xcf\xa4\x44\x7d\xd5\x06\x1a\xdf\x7a\x72\xa8\xb5\xb2\x98\x93\x41\x81\xad\x18\x97\x86\x2c\xe2\xf2\x00\xac\x3e\xa2\xaa\xeb\x9f\xda\x87\x7d\x87\xd2\x4d\xc7\x31\xfc\x55\xa8\x8c\x09\x58\x13\xd2\x33\x41\xe9\x9c\x82\x52\xd1\xd1\x07\xda\x32\x96\xd9\xc6\x80\x5a\xba\x51\x2f\x39\xad\x5f\x33\x4d\x16\xc4\xaa\xb6\x90\xb6\xfd\x35\x1a\x33\xa8\xd7\x6d\xd7\x90\x5e\xa9\x72\x3f\x98\xef\xb5\x9e\xc2\xcf\xbf\xcc\x9f\xb4\xa2\xfc\x19\x97\x0e\x12\x84\x6f\x7f\x64\x5b\x32\x0b\xb9\x46\x66\xd1\x40\x2e\x94\x69\xb4\x97\xb0\xd0\xaa\x06\x92\xb2\xe3\xd4\x71\xa6\x89\xda\xed\xd6\x31\x19\x97\xcc\x94\x93\xb6\x3d\xa8\xd1\x59\xa9\x9f\xeb\xc6\x2f\x08\x75\x63\x62\xc0\xd3\xe9\x1c\x78\xd2\xf1\x8d\x04\xca\x95\x2d\xe7\xc0\x9f\x3f\xef\x89\x2f\xf8\x12\xc6\x1d\xc5\xcf\xfc\x97\xc8\xde\x45\xb4\x0b\xa4\x29\x0c\x77\x73\x1b\xb6\x7c\x4c\x2d\x78\x8e\x63\x7e\x09\x57\x93\x79\x37\x9b\x69\x64\xb7\xdd\x5b\x6b\x47\xff\x9f\xfb\xbb\x9b\x1f\x6a\xc6\x29\xff\x40\x37\xbe\xf6\x37\xc0\x60\xc5\x8d\x85\x46\x0b\x68\x7d\xd8\x9b\xa0\x37\x88\xa3\x1b\x6a\xe5\x04\x97\xed\x43\x8b\xa9\xee\x08\x9e\x4d\x64\x50\x16\xe3\xff\xf8\xf1\xbb\x6f\x23\x63\x35\x97\x2b\xbe\xdc\x8e\xef\x1b\x2d\x66\xf0\x74\x1c\xfc\x4b\xa3\x45\x30\xf9\x79\xfa\x4b\xb4\x66\xa2\xc1\x4b\x67\xef\x99\xfb\x7b\xb2\xcb\x25\xb4\x8f\x33\x38\xdc\x70\x37\x99\xcc\xcf\xf7\x49\x06\x6d\x1d\x8d\x06\xed\x98\x08\x7b\xe0\x1f\xeb\x88\x41\x85\xb6\x54\xce\x75\x35\xe6\x4a\x4a\xcc\x2d\x34\xb5\x92\xad\x4a\x40\x28\x63\xf6\x40\xec\x28\xd2\x53\x50\xb4\xf4\xa9\x0b\xd6\xff\x8d\xd9\x8f\x2a\xbf\x45\x3b\x1e\x8f\x37\x5c\x16\x6a\x13\x09\xe5\xaf\xda\x88\x9c\x54\xe5\x4a\x40\x9a\xa6\xd0\x46\xd1\x60\x02\x5f\x43\xb0\x31\x14\x4f\x03\x98\xd1\x23\x3d\x4d\xe0\x39\x1c\x2f\x2f\x29\xde\x3f\x87\x20\x66\x35\x0f\x26\xde\x1d\x3a\xc5\x2b\x59\xa1\x31\x6c\x85\x43\x01\x5d\x65\xd4\x83\x8c\xce\x51\x99\x15\xa4\xe0\x0c\x54\x33\x6d\xd0\x93\x44\x54\x8d\x77\x68\x23\xcc\x3a\xb2\x34\x05\xd9\x08\xb1\x07\xa9\x77\x8a\x79\x07\xbf\x03\xf2\xc8\xc7\x9a\xaf\xd2\x14\xa8\x34\x25\x15\x17\xfb\x95\x64\x7c\x5f\x44\x4f\x22\x8a\x0b\xfb\x15\x93\xf9\x10\xcd\x07\xdc\xb0\xf8\x3d\x76\x58\x1c\xf3\xc3\xe2\x01\x86\xae\x67\xf1\x18\x3f\xdf\xe3\x18\xb0\x73\x03\x0f\x70\x93\x4d\x95\xa1\x7e\x8c\x9d\xef\x59\xb4\xec\x9c\xaa\xdf\x49\x3b\x58\x7b\x09\x57\xaf\x26\x0f\x70\x47\xad\xd5\x83\xcc\xa5\xb2\xdb\xf1\xbd\x60\x5b\xca\x99\x60\x64\x55\xfd\xd6\xb5\x18\x46\x97\x2e\xe2\xce\xa0\xe7\x70\xe9\x9a\xc7\x33\x18\xb9\x37\x9a\xe7\x15\xba\x55\x2f\xa7\xd3\xe9\x25\x74\x5f\x5d\xfe\xc4\xc8\x09\x75\x83\xbb\x07\xe4\x31\x4d\x9e\x53\xdc\xff\x1c\x89\x5a\x1e\xbd\x4c\xed\xfb\x67\x48\xd5\xc7\x86\x03\xb1\xe0\x0f\x7f\x80\x93\xd9\x43\x18\xc7\x31\xfc\x17\xa3\x32\x5c\x08\xd7\x3d\x70\x4d\x83\x9e\xbe\xe2\xc6\xb8\x62\xdc\x40\xa1\x24\xb6\x6b\x3e\xed\xda\x3f\x91\xb1\x25\x83\x05\x4c\x8f\x05\xa4\xeb\x70\x10\x16\xce\x44\x8b\x01\xdf\xc3\x40\x70\xb1\x1b\xee\x77\xb0\x92\x57\x08\x5f\xa5\x10\x04\xc3\xc5\x27\x14\x44\xd0\x33\xbb\x30\x68\xdf\x7b\x5b\x8c\xdb\xe8\x78\x2e\x76\x4d\x2e\xe1\x66\x3a\x9d\x4e\x4e\x84\xd8\xed\xd5\xfb\xa6\xa6\xb4\x09\x98\xdc\xba\x2b\xb1\xd7\xad\x4b\x1c\x29\x05\xa2\x2b\x4d\x40\xae\x84\xf0\x39\x4b\xbb\x94\x14\xdc\x36\x4f\x52\x08\xaf\xe6\x67\xa2\xe8\x40\x93\x83\xa3\x1d\x9b\xe7\x8c\xee\x8f\x4d\x74\xa8\xb3\x23\xe2\xf0\xea\xc0\x28\x07\xf6\x3a\x6f\x98\x8b\x5e\x6e\xbe\xd7\xe8\x91\xb9\xf6\xf6\x3a\xd6\xd9\x40\x7e\xcf\xe7\xf9\xd5\x47\x1e\xa3\x9f\xae\x1b\x53\x8e\x8f\x04\x9d\xcc\x4f\x6d\xf3\xce\xa2\xa6\x2c\x59\x51\xc8\x22\x5b\x50\x29\xa0\xf1\xc4\x24\x2e\x55\xd7\x18\x6a\x94\x05\xea\x2e\xa5\xf0\x99\x3d\x25\x80\x07\x26\xf3\x55\xe5\x10\x4e\x9f\xe8\x30\x2e\x25\x53\x12\x01\x00\x8e\x9c\xc0\x01\xf5\x00\xa9\x44\x8c\x82\xd5\x06\x0b\x48\xc1\x7f\x04\x1f\x4f\xa2\x46\xf2\xbb\xf1\x24\x6c\xdf\x8f\x79\x74\xf3\xf3\xbe\x4c\xec\xc4\x7e\x9e\x42\x90\x58\x0d\xbc\x48\x47\x01\x3c\x3f\xe7\x82\x14\x75\x47\x8b\xbd\x04\xc3\xa5\x00\x89\x2d\x16\xae\x0f\xea\xeb\xb5\xbf\x07\x19\xcb\x6f\x57\xae\x10\x9a\x51\xaa\x35\x3e\x61\xcb\xd6\xcc\x32\xed\xb8\x4e\xe6\xb0\x27\x6f\x0b\xc5\x9c\x8c\x33\x07\x5f\x91\xba\x76\x2b\xf4\x9f\x28\xdc\x5b\xa6\x74\x81\x3a\xd4\xac\xe0\x8d\x99\xc1\x8b\xfa\x6e\xfe\xf7\xee\x13\x8e\x6b\x0a\x3f\x2a\x6a\xad\x71\x71\x22\x51\xdb\x65\x7c\x0e\x41\x12\x13\xc1\xef\xb1\xe9\x0f\x3b\xfc\xf8\x0e\x67\x5a\xdf\xd0\x7f\x1a\x6f\xc7\x2b\x5e\x14\x02\x49\xe0\x3d\x7b\x72\x46\xb2\xff\xd0\xa5\x0e\xb7\x84\xb6\xe7\xbd\x5f\xb3\x03\x14\x06\x1f\x59\xd0\xb7\xcf\x47\x04\x80\x90\x8e\xcc\x9d\xce\xdb\x62\xdb\x0d\xeb\x91\xd3\x45\xfb\x53\x8a\xa2\xd1\x2e\xd7\x1a\x87\x2d\xc0\x2e\x61\x64\x28\xf7\x2b\xcc\x68\x12\x95\x4d\xc5\x24\xff\x0d\xc7\x14\x97\x26\x5e\x57\xae\x1f\x1f\x9c\x5e\xc9\x27\xc2\xec\x1b\xe5\xa3\x2e\xc6\x8d\x5a\x25\x8e\x3a\xeb\xbe\xd8\xd7\xf6\x33\x98\xce\x47\x9f\xa8\xa1\xf3\xbb\x84\x19\xd3\x30\x7c\x09\xbb\xe0\x0b\x5a\xd1\xee\xdd\x5c\xc6\xf4\xc8\x77\x32\x5c\x7e\x2e\xd5\x26\x1d\xdd\x4c\x7b\x21\xbd\xa1\x9d\x9d\x47\x2d\xd6\x4e\x8c\x41\x52\x76\xae\xb9\x80\x9b\xe9\x97\x90\xd6\x77\x43\x8e\x4e\x60\x35\xaf\xb1\x00\x96\x5b\xbe\xc6\xff\x87\x83\x7c\x01\x25\x7f\xb2\x88\x84\xc3\x4e\x79\x0e\xa6\x07\xf2\xd2\x6c\xaf\xdb\x7f\x25\x7f\x83\xd8\x69\xf8\x39\x04\x67\x0f\xf2\x20\x12\x8f\x08\x8f\x5c\xfb\x61\xbf\x77\x1f\x98\x82\xe3\x98\x42\xd9\x6e\xff\x71\x74\x12\x95\xb6\x12\xe3\x20\xb1\xee\x47\x32\x24\x73\xcf\xc1\x31\xf0\xc3\x87\x29\xdd\xee\xb0\x90\xa1\xfa\x1d\x8f\xea\x2c\x18\x24\x27\x7d\x2d\xd6\x65\x22\xb0\xdb\xff\x96\x28\x8e\xe1\x47\xcb\xb4\x05\x06\x3f\xbd\x83\xa6\x2e\x98\xf5\x9f\x72\x28\x3e\xfa\x4f\x25\xdd\x8f\x8d\x32\xa6\x0d\x2c\x95\xde\x30\x5d\xb4\xfd\x19\x5b\xe2\xd6\x7d\xca\xe9\x52\x3f\x83\xf6\x1d\xdd\x62\x6b\x26\xc6\x27\x75\xdf\xd3\xf1\x28\x1a\x9a\x7c\x34\x89\x90\xe5\xe5\x29\xa1\x8b\x58\xfd\xbe\x29\x7c\xeb\x4a\x80\xf1\xd3\xb1\x2d\xb9\x99\x44\xcc\x5a\x3d\x1e\x1d\x80\x61\x34\x21\xbb\x5e\x0d\x4a\xb2\x7e\x79\x72\xe0\x56\x8f\xf1\xd8\x27\xd3\x7d\x22\xd0\x91\xe7\xc6\x8c\x3d\xae\x46\x97\x03\xde\x87\xb0\x1a\x3d\x1b\xf5\x86\xda\xbb\xf7\xfe\x1c\xe9\x59\x49\x0e\x58\x8f\xc8\xcb\x46\x27\xdb\xb3\xa2\x78\x4b\xfe\x33\x0e\xce\x78\xfa\x31\x3a\x26\xbd\xb2\xfd\x7d\xfd\xa8\x96\xfd\xcf\x32\x1e\x50\x31\x2f\x46\x93\xc8\x34\x99\xef\x4d\x8c\x5f\xf6\x05\x58\x47\xe6\xc0\x7b\x1c\x0a\x4e\x12\x0a\xda\xe2\x30\xa9\x08\x8f\x92\x90\x47\xa2\x46\xbb\xa5\x3f\xd5\xee\x92\x14\x3e\x9d\xf4\xad\xad\x6f\x0c\x25\x57\xbe\xf5\xbf\xc1\xcc\xb8\x4e\x02\xb4\x78\x77\xdd\x1c\xdf\xb5\x79\xf3\xfd\xbb\x41\xe7\xa6\xf7\x88\xb1\xe3\xde\xff\x0e\xf0\x5c\x9f\xe4\xec\x0f\x0f\x37\x9b\x4d\xb4\x52\x6a\x25\xfc\x4f\x0e\xfb\x46\x4a\xcc\x6a\x1e\x7d\x30\x01\x30\xb3\x95\x39\x14\xb8\x44\xbd\x18\xb0\x6f\xbb\x2b\x49\xec\x7f\x12\x97\xc4\xfe\x57\xbf\xff\x17\x00\x00\xff\xff\x31\x9f\x54\x5e\x06\x2c\x00\x00") func faucetHtmlBytes() ([]byte, error) { return bindataRead( @@ -83,7 +85,7 @@ func faucetHtml() (*asset, error) { } info := bindataFileInfo{name: "faucet.html", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} - a := &asset{bytes: bytes, info: info} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xdb, 0xa2, 0x98, 0x44, 0x4b, 0x50, 0xf8, 0xa1, 0xac, 0x4a, 0x76, 0x2e, 0xcc, 0x3d, 0xcb, 0x81, 0x9e, 0x2a, 0xaa, 0x87, 0xf5, 0x9d, 0x53, 0x4, 0x8a, 0xdd, 0x5a, 0xfe, 0xd3, 0xc3, 0xf, 0x11}} return a, nil } @@ -102,6 +104,12 @@ func Asset(name string) ([]byte, error) { return nil, fmt.Errorf("Asset %s not found", name) } +// AssetString returns the asset contents as a string (instead of a []byte). +func AssetString(name string) (string, error) { + data, err := Asset(name) + return string(data), err +} + // MustAsset is like Asset but panics when Asset would return an error. // It simplifies safe initialization of global variables. func MustAsset(name string) []byte { @@ -113,6 +121,12 @@ func MustAsset(name string) []byte { return a } +// MustAssetString is like AssetString but panics when Asset would return an +// error. It simplifies safe initialization of global variables. +func MustAssetString(name string) string { + return string(MustAsset(name)) +} + // AssetInfo loads and returns the asset info for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -128,6 +142,33 @@ func AssetInfo(name string) (os.FileInfo, error) { return nil, fmt.Errorf("AssetInfo %s not found", name) } +// AssetDigest returns the digest of the file with the given name. It returns an +// error if the asset could not be found or the digest could not be loaded. +func AssetDigest(name string) ([sha256.Size]byte, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) + } + return a.digest, nil + } + return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) +} + +// Digests returns a map of all known files and their checksums. +func Digests() (map[string][sha256.Size]byte, error) { + mp := make(map[string][sha256.Size]byte, len(_bindata)) + for name := range _bindata { + a, err := _bindata[name]() + if err != nil { + return nil, err + } + mp[name] = a.digest + } + return mp, nil +} + // AssetNames returns the names of the assets. func AssetNames() []string { names := make([]string, 0, len(_bindata)) @@ -151,9 +192,9 @@ var _bindata = map[string]func() (*asset, error){ // img/ // a.png // b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// then AssetDir("data") would return []string{"foo.txt", "img"}, +// AssetDir("data/img") would return []string{"a.png", "b.png"}, +// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and // AssetDir("") will return []string{"data"}. func AssetDir(name string) ([]string, error) { node := _bintree @@ -186,7 +227,7 @@ var _bintree = &bintree{nil, map[string]*bintree{ "faucet.html": {faucetHtml, map[string]*bintree{}}, }} -// RestoreAsset restores an asset under the given directory +// RestoreAsset restores an asset under the given directory. func RestoreAsset(dir, name string) error { data, err := Asset(name) if err != nil { @@ -207,7 +248,7 @@ func RestoreAsset(dir, name string) error { return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) } -// RestoreAssets restores an asset under the given directory recursively +// RestoreAssets restores an asset under the given directory recursively. func RestoreAssets(dir, name string) error { children, err := AssetDir(name) // File From 9effd642901e13765dcc1396392ba55a18f66ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 13 May 2019 15:28:01 +0300 Subject: [PATCH 10/34] core, eth, trie: bloom filter for trie node dedup during fast sync (#19489) * core, eth, trie: bloom filter for trie node dedup during fast sync * eth/downloader, trie: address review comments * core, ethdb, trie: restart fast-sync bloom construction now and again * eth/downloader: initialize fast sync bloom on startup * eth: reenable eth/62 until we properly remove it --- cmd/geth/chaincmd.go | 14 +- core/rawdb/table.go | 7 + core/state/sync.go | 4 +- core/state/sync_test.go | 13 +- eth/backend.go | 5 +- eth/downloader/downloader.go | 36 +- eth/downloader/downloader_test.go | 2 +- eth/downloader/statesync.go | 3 +- eth/handler.go | 25 +- eth/handler_test.go | 4 +- eth/helper_test.go | 3 +- ethdb/iterator.go | 5 + ethdb/leveldb/leveldb.go | 9 +- ethdb/memorydb/memorydb.go | 31 +- les/handler.go | 2 +- trie/sync.go | 37 +- trie/sync_bloom.go | 207 ++++++++++ trie/sync_test.go | 14 +- .../steakknife/bloomfilter/MIT-LICENSE.txt | 8 + .../steakknife/bloomfilter/README.md | 123 ++++++ .../steakknife/bloomfilter/binarymarshaler.go | 87 ++++ .../bloomfilter/binaryunmarshaler.go | 111 ++++++ .../steakknife/bloomfilter/bloomfilter.go | 123 ++++++ .../steakknife/bloomfilter/conformance.go | 29 ++ .../steakknife/bloomfilter/debug.go | 37 ++ .../steakknife/bloomfilter/errors.go | 34 ++ .../steakknife/bloomfilter/fileio.go | 105 +++++ .../github.com/steakknife/bloomfilter/gob.go | 23 ++ .../steakknife/bloomfilter/iscompatible.go | 41 ++ .../github.com/steakknife/bloomfilter/new.go | 134 +++++++ .../steakknife/bloomfilter/optimal.go | 28 ++ .../steakknife/bloomfilter/statistics.go | 43 ++ .../steakknife/bloomfilter/textmarshaler.go | 49 +++ .../steakknife/bloomfilter/textunmarshaler.go | 150 +++++++ .../steakknife/hamming/MIT-LICENSE.txt | 8 + .../github.com/steakknife/hamming/README.md | 82 ++++ vendor/github.com/steakknife/hamming/doc.go | 35 ++ .../github.com/steakknife/hamming/hamming.go | 70 ++++ .../steakknife/hamming/popcnt_amd64.go | 65 +++ .../steakknife/hamming/popcnt_amd64.s | 64 +++ .../github.com/steakknife/hamming/popcount.go | 134 +++++++ .../steakknife/hamming/popcount_slices.go | 123 ++++++ .../hamming/popcount_slices_amd64.go | 72 ++++ .../hamming/popcount_slices_amd64.s | 370 ++++++++++++++++++ .../steakknife/hamming/slices_of_hamming.go | 144 +++++++ vendor/vendor.json | 12 + 46 files changed, 2668 insertions(+), 57 deletions(-) create mode 100644 trie/sync_bloom.go create mode 100644 vendor/github.com/steakknife/bloomfilter/MIT-LICENSE.txt create mode 100644 vendor/github.com/steakknife/bloomfilter/README.md create mode 100644 vendor/github.com/steakknife/bloomfilter/binarymarshaler.go create mode 100644 vendor/github.com/steakknife/bloomfilter/binaryunmarshaler.go create mode 100644 vendor/github.com/steakknife/bloomfilter/bloomfilter.go create mode 100644 vendor/github.com/steakknife/bloomfilter/conformance.go create mode 100644 vendor/github.com/steakknife/bloomfilter/debug.go create mode 100644 vendor/github.com/steakknife/bloomfilter/errors.go create mode 100644 vendor/github.com/steakknife/bloomfilter/fileio.go create mode 100644 vendor/github.com/steakknife/bloomfilter/gob.go create mode 100644 vendor/github.com/steakknife/bloomfilter/iscompatible.go create mode 100644 vendor/github.com/steakknife/bloomfilter/new.go create mode 100644 vendor/github.com/steakknife/bloomfilter/optimal.go create mode 100644 vendor/github.com/steakknife/bloomfilter/statistics.go create mode 100644 vendor/github.com/steakknife/bloomfilter/textmarshaler.go create mode 100644 vendor/github.com/steakknife/bloomfilter/textunmarshaler.go create mode 100644 vendor/github.com/steakknife/hamming/MIT-LICENSE.txt create mode 100644 vendor/github.com/steakknife/hamming/README.md create mode 100644 vendor/github.com/steakknife/hamming/doc.go create mode 100644 vendor/github.com/steakknife/hamming/hamming.go create mode 100644 vendor/github.com/steakknife/hamming/popcnt_amd64.go create mode 100644 vendor/github.com/steakknife/hamming/popcnt_amd64.s create mode 100644 vendor/github.com/steakknife/hamming/popcount.go create mode 100644 vendor/github.com/steakknife/hamming/popcount_slices.go create mode 100644 vendor/github.com/steakknife/hamming/popcount_slices_amd64.go create mode 100644 vendor/github.com/steakknife/hamming/popcount_slices_amd64.s create mode 100644 vendor/github.com/steakknife/hamming/slices_of_hamming.go diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 21e4017fa6..582f0b768e 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/trie" "gopkg.in/urfave/cli.v1" ) @@ -375,11 +376,16 @@ func copyDb(ctx *cli.Context) error { defer stack.Close() chain, chainDb := utils.MakeChain(ctx, stack) - syncmode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode) - dl := downloader.New(syncmode, 0, chainDb, new(event.TypeMux), chain, nil, nil) + syncMode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode) + + var syncBloom *trie.SyncBloom + if syncMode == downloader.FastSync { + syncBloom = trie.NewSyncBloom(uint64(ctx.GlobalInt(utils.CacheFlag.Name)/2), chainDb) + } + dl := downloader.New(0, chainDb, syncBloom, new(event.TypeMux), chain, nil, nil) // Create a source peer to satisfy downloader requests from - db, err := rawdb.NewLevelDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name), 256, "") + db, err := rawdb.NewLevelDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name)/2, 256, "") if err != nil { return err } @@ -395,7 +401,7 @@ func copyDb(ctx *cli.Context) error { start := time.Now() currentHeader := hc.CurrentHeader() - if err = dl.Synchronise("local", currentHeader.Hash(), hc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()), syncmode); err != nil { + if err = dl.Synchronise("local", currentHeader.Hash(), hc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()), syncMode); err != nil { return err } for dl.Synchronising() { diff --git a/core/rawdb/table.go b/core/rawdb/table.go index e19649dd46..0e50db7c90 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -67,6 +67,13 @@ func (t *table) NewIterator() ethdb.Iterator { return t.NewIteratorWithPrefix(nil) } +// NewIteratorWithStart creates a binary-alphabetical iterator over a subset of +// database content starting at a particular initial key (or after, if it does +// not exist). +func (t *table) NewIteratorWithStart(start []byte) ethdb.Iterator { + return t.db.NewIteratorWithStart(start) +} + // NewIteratorWithPrefix creates a binary-alphabetical iterator over a subset // of database content with a particular key prefix. func (t *table) NewIteratorWithPrefix(prefix []byte) ethdb.Iterator { diff --git a/core/state/sync.go b/core/state/sync.go index 5290411a3b..e4a08d2938 100644 --- a/core/state/sync.go +++ b/core/state/sync.go @@ -26,7 +26,7 @@ import ( ) // NewStateSync create a new state trie download scheduler. -func NewStateSync(root common.Hash, database ethdb.Reader) *trie.Sync { +func NewStateSync(root common.Hash, database ethdb.Reader, bloom *trie.SyncBloom) *trie.Sync { var syncer *trie.Sync callback := func(leaf []byte, parent common.Hash) error { var obj Account @@ -37,6 +37,6 @@ func NewStateSync(root common.Hash, database ethdb.Reader) *trie.Sync { syncer.AddRawEntry(common.BytesToHash(obj.CodeHash), 64, parent) return nil } - syncer = trie.NewSync(root, database, callback) + syncer = trie.NewSync(root, database, callback, bloom) return syncer } diff --git a/core/state/sync_test.go b/core/state/sync_test.go index ab4718b04c..de098dce0f 100644 --- a/core/state/sync_test.go +++ b/core/state/sync_test.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/trie" ) @@ -125,7 +126,7 @@ func checkStateConsistency(db ethdb.Database, root common.Hash) error { // Tests that an empty state is not scheduled for syncing. func TestEmptyStateSync(t *testing.T) { empty := common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") - if req := NewStateSync(empty, rawdb.NewMemoryDatabase()).Missing(1); len(req) != 0 { + if req := NewStateSync(empty, rawdb.NewMemoryDatabase(), trie.NewSyncBloom(1, memorydb.New())).Missing(1); len(req) != 0 { t.Errorf("content requested for empty state: %v", req) } } @@ -141,7 +142,7 @@ func testIterativeStateSync(t *testing.T, batch int) { // Create a destination state and sync with the scheduler dstDb := rawdb.NewMemoryDatabase() - sched := NewStateSync(srcRoot, dstDb) + sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) queue := append([]common.Hash{}, sched.Missing(batch)...) for len(queue) > 0 { @@ -173,7 +174,7 @@ func TestIterativeDelayedStateSync(t *testing.T) { // Create a destination state and sync with the scheduler dstDb := rawdb.NewMemoryDatabase() - sched := NewStateSync(srcRoot, dstDb) + sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) queue := append([]common.Hash{}, sched.Missing(0)...) for len(queue) > 0 { @@ -210,7 +211,7 @@ func testIterativeRandomStateSync(t *testing.T, batch int) { // Create a destination state and sync with the scheduler dstDb := rawdb.NewMemoryDatabase() - sched := NewStateSync(srcRoot, dstDb) + sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) queue := make(map[common.Hash]struct{}) for _, hash := range sched.Missing(batch) { @@ -250,7 +251,7 @@ func TestIterativeRandomDelayedStateSync(t *testing.T) { // Create a destination state and sync with the scheduler dstDb := rawdb.NewMemoryDatabase() - sched := NewStateSync(srcRoot, dstDb) + sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) queue := make(map[common.Hash]struct{}) for _, hash := range sched.Missing(0) { @@ -297,7 +298,7 @@ func TestIncompleteStateSync(t *testing.T) { // Create a destination state and sync with the scheduler dstDb := rawdb.NewMemoryDatabase() - sched := NewStateSync(srcRoot, dstDb) + sched := NewStateSync(srcRoot, dstDb, trie.NewSyncBloom(1, dstDb)) added := []common.Hash{} queue := append([]common.Hash{}, sched.Missing(1)...) diff --git a/eth/backend.go b/eth/backend.go index 3363054aa8..f696157763 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -190,10 +190,11 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { } eth.txPool = core.NewTxPool(config.TxPool, chainConfig, eth.blockchain) - if eth.protocolManager, err = NewProtocolManager(chainConfig, config.SyncMode, config.NetworkId, eth.eventMux, eth.txPool, eth.engine, eth.blockchain, chainDb, config.Whitelist); err != nil { + // Permit the downloader to use the trie cache allowance during fast sync + cacheLimit := cacheConfig.TrieCleanLimit + cacheConfig.TrieDirtyLimit + if eth.protocolManager, err = NewProtocolManager(chainConfig, config.SyncMode, config.NetworkId, eth.eventMux, eth.txPool, eth.engine, eth.blockchain, chainDb, cacheLimit, config.Whitelist); err != nil { return nil, err } - eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock) eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 95f2c2aeec..0e19fe9e65 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" ) var ( @@ -104,7 +105,9 @@ type Downloader struct { genesis uint64 // Genesis block number to limit sync to (e.g. light client CHT) queue *queue // Scheduler for selecting the hashes to download peers *peerSet // Set of active peers from which download can proceed - stateDB ethdb.Database + + stateDB ethdb.Database // Database to state sync into (and deduplicate via) + stateBloom *trie.SyncBloom // Bloom filter for fast trie node existence checks rttEstimate uint64 // Round trip time to target for download requests rttConfidence uint64 // Confidence in the estimated RTT (unit: millionths to allow atomic ops) @@ -207,13 +210,13 @@ type BlockChain interface { } // New creates a new downloader to fetch hashes and blocks from remote peers. -func New(mode SyncMode, checkpoint uint64, stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn) *Downloader { +func New(checkpoint uint64, stateDb ethdb.Database, stateBloom *trie.SyncBloom, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn) *Downloader { if lightchain == nil { lightchain = chain } dl := &Downloader{ - mode: mode, stateDB: stateDb, + stateBloom: stateBloom, mux: mux, checkpoint: checkpoint, queue: newQueue(), @@ -255,13 +258,15 @@ func (d *Downloader) Progress() ethereum.SyncProgress { defer d.syncStatsLock.RUnlock() current := uint64(0) - switch d.mode { - case FullSync: + switch { + case d.blockchain != nil && d.mode == FullSync: current = d.blockchain.CurrentBlock().NumberU64() - case FastSync: + case d.blockchain != nil && d.mode == FastSync: current = d.blockchain.CurrentFastBlock().NumberU64() - case LightSync: + case d.lightchain != nil: current = d.lightchain.CurrentHeader().Number.Uint64() + default: + log.Error("Unknown downloader chain/mode combo", "light", d.lightchain != nil, "full", d.blockchain != nil, "mode", d.mode) } return ethereum.SyncProgress{ StartingBlock: d.syncStatsChainOrigin, @@ -363,6 +368,12 @@ func (d *Downloader) synchronise(id string, hash common.Hash, td *big.Int, mode if atomic.CompareAndSwapInt32(&d.notified, 0, 1) { log.Info("Block synchronisation started") } + // If we are already full syncing, but have a fast-sync bloom filter laying + // around, make sure it does't use memory any more. This is a special case + // when the user attempts to fast sync a new empty network. + if mode == FullSync && d.stateBloom != nil { + d.stateBloom.Close() + } // Reset the queue, peer set and wake channels to clean any internal leftover state d.queue.Reset() d.peers.Reset() @@ -1662,6 +1673,8 @@ func (d *Downloader) commitFastSyncData(results []*fetchResult, stateSync *state func (d *Downloader) commitPivotBlock(result *fetchResult) error { block := types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles) log.Debug("Committing fast sync pivot as new head", "number", block.Number(), "hash", block.Hash()) + + // Commit the pivot block as the new head, will require full sync from here on if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []types.Receipts{result.Receipts}); err != nil { return err } @@ -1669,6 +1682,15 @@ func (d *Downloader) commitPivotBlock(result *fetchResult) error { return err } atomic.StoreInt32(&d.committed, 1) + + // If we had a bloom filter for the state sync, deallocate it now. Note, we only + // deallocate internally, but keep the empty wrapper. This ensures that if we do + // a rollback after committing the pivot and restarting fast sync, we don't end + // up using a nil bloom. Empty bloom is fine, it just returns that it does not + // have the info we need, so reach down to the database instead. + if d.stateBloom != nil { + d.stateBloom.Close() + } return nil } diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index ec45eb06dd..ac7f48abdb 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -75,7 +75,7 @@ func newTester() *downloadTester { tester.stateDb = rawdb.NewMemoryDatabase() tester.stateDb.Put(testGenesis.Root().Bytes(), []byte{0x00}) - tester.downloader = New(FullSync, 0, tester.stateDb, new(event.TypeMux), tester, nil, tester.dropPeer) + tester.downloader = New(0, tester.stateDb, trie.NewSyncBloom(1, tester.stateDb), new(event.TypeMux), tester, nil, tester.dropPeer) return tester } diff --git a/eth/downloader/statesync.go b/eth/downloader/statesync.go index f294600b37..7e156f46e7 100644 --- a/eth/downloader/statesync.go +++ b/eth/downloader/statesync.go @@ -59,6 +59,7 @@ type stateSyncStats struct { // syncState starts downloading state with the given root hash. func (d *Downloader) syncState(root common.Hash) *stateSync { + // Create the state sync s := newStateSync(d, root) select { case d.stateSyncStart <- s: @@ -239,7 +240,7 @@ type stateTask struct { func newStateSync(d *Downloader, root common.Hash) *stateSync { return &stateSync{ d: d, - sched: state.NewStateSync(root, d.stateDB), + sched: state.NewStateSync(root, d.stateDB, d.stateBloom), keccak: sha3.NewLegacyKeccak256(), tasks: make(map[common.Hash]*stateTask), deliver: make(chan *stateReq), diff --git a/eth/handler.go b/eth/handler.go index 7b753ebb44..58add2eafc 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" ) const ( @@ -105,7 +106,7 @@ type ProtocolManager struct { // NewProtocolManager returns a new Ethereum sub protocol manager. The Ethereum sub protocol manages peers capable // with the Ethereum network. -func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, networkID uint64, mux *event.TypeMux, txpool txPool, engine consensus.Engine, blockchain *core.BlockChain, chaindb ethdb.Database, whitelist map[uint64]common.Hash) (*ProtocolManager, error) { +func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, networkID uint64, mux *event.TypeMux, txpool txPool, engine consensus.Engine, blockchain *core.BlockChain, chaindb ethdb.Database, cacheLimit int, whitelist map[uint64]common.Hash) (*ProtocolManager, error) { // Create the protocol manager with the base fields manager := &ProtocolManager{ networkID: networkID, @@ -120,12 +121,8 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne txsyncCh: make(chan *txsync), quitSync: make(chan struct{}), } - // Figure out whether to allow fast sync or not - if mode == downloader.FastSync && blockchain.CurrentBlock().NumberU64() > 0 { - log.Warn("Blockchain not empty, fast sync disabled") - mode = downloader.FullSync - } - if mode == downloader.FastSync { + // If fast sync was requested and our database is empty, grant it + if mode == downloader.FastSync && blockchain.CurrentBlock().NumberU64() == 0 { manager.fastSync = uint32(1) } // If we have trusted checkpoints, enforce them on the chain @@ -137,7 +134,8 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne manager.SubProtocols = make([]p2p.Protocol, 0, len(ProtocolVersions)) for i, version := range ProtocolVersions { // Skip protocol version if incompatible with the mode of operation - if mode == downloader.FastSync && version < eth63 { + // TODO(karalabe): hard-drop eth/62 from the code base + if atomic.LoadUint32(&manager.fastSync) == 1 && version < eth63 { continue } // Compatible; initialise the sub-protocol @@ -171,9 +169,16 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne if len(manager.SubProtocols) == 0 { return nil, errIncompatibleConfig } - // Construct the different synchronisation mechanisms - manager.downloader = downloader.New(mode, manager.checkpointNumber, chaindb, manager.eventMux, blockchain, nil, manager.removePeer) + // Construct the downloader (long sync) and its backing state bloom if fast + // sync is requested. The downloader is responsible for deallocating the state + // bloom when it's done. + var stateBloom *trie.SyncBloom + if atomic.LoadUint32(&manager.fastSync) == 1 { + stateBloom = trie.NewSyncBloom(uint64(cacheLimit), chaindb) + } + manager.downloader = downloader.New(manager.checkpointNumber, chaindb, stateBloom, manager.eventMux, blockchain, nil, manager.removePeer) + // Construct the fetcher (short sync) validator := func(header *types.Header) error { return engine.VerifyHeader(blockchain, header, true) } diff --git a/eth/handler_test.go b/eth/handler_test.go index 72a0b78aba..04831e4dc6 100644 --- a/eth/handler_test.go +++ b/eth/handler_test.go @@ -528,7 +528,7 @@ func testCheckpointChallenge(t *testing.T, syncmode downloader.SyncMode, checkpo if err != nil { t.Fatalf("failed to create new blockchain: %v", err) } - pm, err := NewProtocolManager(config, syncmode, DefaultConfig.NetworkId, new(event.TypeMux), new(testTxPool), ethash.NewFaker(), blockchain, db, nil) + pm, err := NewProtocolManager(config, syncmode, DefaultConfig.NetworkId, new(event.TypeMux), new(testTxPool), ethash.NewFaker(), blockchain, db, 1, nil) if err != nil { t.Fatalf("failed to start test protocol manager: %v", err) } @@ -615,7 +615,7 @@ func testBroadcastBlock(t *testing.T, totalPeers, broadcastExpected int) { if err != nil { t.Fatalf("failed to create new blockchain: %v", err) } - pm, err := NewProtocolManager(config, downloader.FullSync, DefaultConfig.NetworkId, evmux, new(testTxPool), pow, blockchain, db, nil) + pm, err := NewProtocolManager(config, downloader.FullSync, DefaultConfig.NetworkId, evmux, new(testTxPool), pow, blockchain, db, 1, nil) if err != nil { t.Fatalf("failed to start test protocol manager: %v", err) } diff --git a/eth/helper_test.go b/eth/helper_test.go index e91429b8c9..27e7189ed9 100644 --- a/eth/helper_test.go +++ b/eth/helper_test.go @@ -66,8 +66,7 @@ func newTestProtocolManager(mode downloader.SyncMode, blocks int, generator func if _, err := blockchain.InsertChain(chain); err != nil { panic(err) } - - pm, err := NewProtocolManager(gspec.Config, mode, DefaultConfig.NetworkId, evmux, &testTxPool{added: newtx}, engine, blockchain, db, nil) + pm, err := NewProtocolManager(gspec.Config, mode, DefaultConfig.NetworkId, evmux, &testTxPool{added: newtx}, engine, blockchain, db, 1, nil) if err != nil { return nil, nil, err } diff --git a/ethdb/iterator.go b/ethdb/iterator.go index f3cee7ec9c..419e9bdfc2 100644 --- a/ethdb/iterator.go +++ b/ethdb/iterator.go @@ -55,6 +55,11 @@ type Iteratee interface { // contained within the key-value database. NewIterator() Iterator + // NewIteratorWithStart creates a binary-alphabetical iterator over a subset of + // database content starting at a particular initial key (or after, if it does + // not exist). + NewIteratorWithStart(start []byte) Iterator + // NewIteratorWithPrefix creates a binary-alphabetical iterator over a subset // of database content with a particular key prefix. NewIteratorWithPrefix(prefix []byte) Iterator diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index f437cb9740..8eabee50e2 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -175,7 +175,14 @@ func (db *Database) NewBatch() ethdb.Batch { // NewIterator creates a binary-alphabetical iterator over the entire keyspace // contained within the leveldb database. func (db *Database) NewIterator() ethdb.Iterator { - return db.NewIteratorWithPrefix(nil) + return db.db.NewIterator(new(util.Range), nil) +} + +// NewIteratorWithStart creates a binary-alphabetical iterator over a subset of +// database content starting at a particular initial key (or after, if it does +// not exist). +func (db *Database) NewIteratorWithStart(start []byte) ethdb.Iterator { + return db.db.NewIterator(&util.Range{Start: start}, nil) } // NewIteratorWithPrefix creates a binary-alphabetical iterator over a subset diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index 5c3f7e22a3..cb8b27f3b3 100644 --- a/ethdb/memorydb/memorydb.go +++ b/ethdb/memorydb/memorydb.go @@ -132,7 +132,36 @@ func (db *Database) NewBatch() ethdb.Batch { // NewIterator creates a binary-alphabetical iterator over the entire keyspace // contained within the memory database. func (db *Database) NewIterator() ethdb.Iterator { - return db.NewIteratorWithPrefix(nil) + return db.NewIteratorWithStart(nil) +} + +// NewIteratorWithStart creates a binary-alphabetical iterator over a subset of +// database content starting at a particular initial key (or after, if it does +// not exist). +func (db *Database) NewIteratorWithStart(start []byte) ethdb.Iterator { + db.lock.RLock() + defer db.lock.RUnlock() + + var ( + st = string(start) + keys = make([]string, 0, len(db.db)) + values = make([][]byte, 0, len(db.db)) + ) + // Collect the keys from the memory database corresponding to the given start + for key := range db.db { + if key >= st { + keys = append(keys, key) + } + } + // Sort the items and retrieve the associated values + sort.Strings(keys) + for _, key := range keys { + values = append(values, db.db[key]) + } + return &iterator{ + keys: keys, + values: values, + } } // NewIteratorWithPrefix creates a binary-alphabetical iterator over a subset diff --git a/les/handler.go b/les/handler.go index c6baeece4d..0d235e7a52 100644 --- a/les/handler.go +++ b/les/handler.go @@ -179,7 +179,7 @@ func NewProtocolManager( if cht, ok := params.TrustedCheckpoints[blockchain.Genesis().Hash()]; ok { checkpoint = (cht.SectionIndex+1)*params.CHTFrequency - 1 } - manager.downloader = downloader.New(downloader.LightSync, checkpoint, chainDb, manager.eventMux, nil, blockchain, removePeer) + manager.downloader = downloader.New(checkpoint, chainDb, nil, manager.eventMux, nil, blockchain, removePeer) manager.peers.notify((*downloaderPeerNotify)(manager)) manager.fetcher = newLightFetcher(manager) } diff --git a/trie/sync.go b/trie/sync.go index 85f1b0f850..d9564d7831 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -76,15 +76,17 @@ type Sync struct { membatch *syncMemBatch // Memory buffer to avoid frequent database writes requests map[common.Hash]*request // Pending requests pertaining to a key hash queue *prque.Prque // Priority queue with the pending requests + bloom *SyncBloom // Bloom filter for fast node existence checks } // NewSync creates a new trie data download scheduler. -func NewSync(root common.Hash, database ethdb.Reader, callback LeafCallback) *Sync { +func NewSync(root common.Hash, database ethdb.Reader, callback LeafCallback, bloom *SyncBloom) *Sync { ts := &Sync{ database: database, membatch: newSyncMemBatch(), requests: make(map[common.Hash]*request), queue: prque.New(nil), + bloom: bloom, } ts.AddSubTrie(root, 0, common.Hash{}, callback) return ts @@ -99,10 +101,14 @@ func (s *Sync) AddSubTrie(root common.Hash, depth int, parent common.Hash, callb if _, ok := s.membatch.batch[root]; ok { return } - key := root.Bytes() - blob, _ := s.database.Get(key) - if local, err := decodeNode(key, blob); local != nil && err == nil { - return + if s.bloom.Contains(root[:]) { + // Bloom filter says this might be a duplicate, double check + blob, _ := s.database.Get(root[:]) + if local, err := decodeNode(root[:], blob); local != nil && err == nil { + return + } + // False positive, bump fault meter + bloomFaultMeter.Mark(1) } // Assemble the new sub-trie sync request req := &request{ @@ -134,8 +140,13 @@ func (s *Sync) AddRawEntry(hash common.Hash, depth int, parent common.Hash) { if _, ok := s.membatch.batch[hash]; ok { return } - if ok, _ := s.database.Has(hash.Bytes()); ok { - return + if s.bloom.Contains(hash[:]) { + // Bloom filter says this might be a duplicate, double check + if ok, _ := s.database.Has(hash[:]); ok { + return + } + // False positive, bump fault meter + bloomFaultMeter.Mark(1) } // Assemble the new sub-trie sync request req := &request{ @@ -219,8 +230,9 @@ func (s *Sync) Commit(dbw ethdb.Writer) (int, error) { if err := dbw.Put(key[:], s.membatch.batch[key]); err != nil { return i, err } + s.bloom.Add(key[:]) } - written := len(s.membatch.order) + written := len(s.membatch.order) // TODO(karalabe): could an order change improve write performance? // Drop the membatch data and return s.membatch = newSyncMemBatch() @@ -292,8 +304,13 @@ func (s *Sync) children(req *request, object node) ([]*request, error) { if _, ok := s.membatch.batch[hash]; ok { continue } - if ok, _ := s.database.Has(node); ok { - continue + if s.bloom.Contains(node) { + // Bloom filter says this might be a duplicate, double check + if ok, _ := s.database.Has(node); ok { + continue + } + // False positive, bump fault meter + bloomFaultMeter.Mark(1) } // Locally unknown node, schedule for retrieval requests = append(requests, &request{ diff --git a/trie/sync_bloom.go b/trie/sync_bloom.go new file mode 100644 index 0000000000..899a63add8 --- /dev/null +++ b/trie/sync_bloom.go @@ -0,0 +1,207 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "encoding/binary" + "fmt" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/steakknife/bloomfilter" +) + +var ( + bloomAddMeter = metrics.NewRegisteredMeter("trie/bloom/add", nil) + bloomLoadMeter = metrics.NewRegisteredMeter("trie/bloom/load", nil) + bloomTestMeter = metrics.NewRegisteredMeter("trie/bloom/test", nil) + bloomMissMeter = metrics.NewRegisteredMeter("trie/bloom/miss", nil) + bloomFaultMeter = metrics.NewRegisteredMeter("trie/bloom/fault", nil) + bloomErrorGauge = metrics.NewRegisteredGauge("trie/bloom/error", nil) +) + +// syncBloomHasher is a wrapper around a byte blob to satisfy the interface API +// requirements of the bloom library used. It's used to convert a trie hash into +// a 64 bit mini hash. +type syncBloomHasher []byte + +func (f syncBloomHasher) Write(p []byte) (n int, err error) { panic("not implemented") } +func (f syncBloomHasher) Sum(b []byte) []byte { panic("not implemented") } +func (f syncBloomHasher) Reset() { panic("not implemented") } +func (f syncBloomHasher) BlockSize() int { panic("not implemented") } +func (f syncBloomHasher) Size() int { return 8 } +func (f syncBloomHasher) Sum64() uint64 { return binary.BigEndian.Uint64(f) } + +// SyncBloom is a bloom filter used during fast sync to quickly decide if a trie +// node already exists on disk or not. It self populates from the provided disk +// database on creation in a background thread and will only start returning live +// results once that's finished. +type SyncBloom struct { + bloom *bloomfilter.Filter + inited uint32 + closer sync.Once + closed uint32 + pend sync.WaitGroup +} + +// NewSyncBloom creates a new bloom filter of the given size (in megabytes) and +// initializes it from the database. The bloom is hard coded to use 3 filters. +func NewSyncBloom(memory uint64, database ethdb.Iteratee) *SyncBloom { + // Create the bloom filter to track known trie nodes + bloom, err := bloomfilter.New(memory*1024*1024*8, 3) + if err != nil { + panic(fmt.Sprintf("failed to create bloom: %v", err)) // Can't happen, here for sanity + } + log.Info("Allocated fast sync bloom", "size", common.StorageSize(memory*1024*1024)) + + // Assemble the fast sync bloom and init it from previous sessions + b := &SyncBloom{ + bloom: bloom, + } + b.pend.Add(2) + go func() { + defer b.pend.Done() + b.init(database) + }() + go func() { + defer b.pend.Done() + b.meter() + }() + return b +} + +// init iterates over the database, pushing every trie hash into the bloom filter. +func (b *SyncBloom) init(database ethdb.Iteratee) { + // Iterate over the database, but restart every now and again to avoid holding + // a persistent snapshot since fast sync can push a ton of data concurrently, + // bloating the disk. + // + // Note, this is fine, because everything inserted into leveldb by fast sync is + // also pushed into the bloom directly, so we're not missing anything when the + // iterator is swapped out for a new one. + it := database.NewIterator() + + var ( + start = time.Now() + swap = time.Now() + ) + for it.Next() && atomic.LoadUint32(&b.closed) == 0 { + // If the database entry is a trie node, add it to the bloom + if key := it.Key(); len(key) == common.HashLength { + b.bloom.Add(syncBloomHasher(key)) + bloomLoadMeter.Mark(1) + } + // If enough time elapsed since the last iterator swap, restart + if time.Since(swap) > 8*time.Second { + key := common.CopyBytes(it.Key()) + + it.Release() + it = database.NewIteratorWithStart(key) + + log.Info("Initializing fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate(), "elapsed", time.Since(start)) + swap = time.Now() + } + } + it.Release() + + // Mark the bloom filter inited and return + log.Info("Initialized fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate(), "elapsed", time.Since(start)) + atomic.StoreUint32(&b.inited, 1) +} + +// meter periodically recalculates the false positive error rate of the bloom +// filter and reports it in a metric. +func (b *SyncBloom) meter() { + for { + // Report the current error ration. No floats, lame, scale it up. + bloomErrorGauge.Update(int64(b.errorRate() * 100000)) + + // Wait one second, but check termination more frequently + for i := 0; i < 10; i++ { + if atomic.LoadUint32(&b.closed) == 1 { + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +// Close terminates any background initializer still running and releases all the +// memory allocated for the bloom. +func (b *SyncBloom) Close() error { + b.closer.Do(func() { + // Ensure the initializer is stopped + atomic.StoreUint32(&b.closed, 1) + b.pend.Wait() + + // Wipe the bloom, but mark it "uninited" just in case someone attempts an access + log.Info("Deallocated fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate()) + + atomic.StoreUint32(&b.inited, 0) + b.bloom = nil + }) + return nil +} + +// Add inserts a new trie node hash into the bloom filter. +func (b *SyncBloom) Add(hash []byte) { + if atomic.LoadUint32(&b.closed) == 1 { + return + } + b.bloom.Add(syncBloomHasher(hash)) + bloomAddMeter.Mark(1) +} + +// Contains tests if the bloom filter contains the given hash: +// - false: the bloom definitely does not contain hash +// - true: the bloom maybe contains hash +// +// While the bloom is being initialized, any query will return true. +func (b *SyncBloom) Contains(hash []byte) bool { + bloomTestMeter.Mark(1) + if atomic.LoadUint32(&b.inited) == 0 { + // We didn't load all the trie nodes from the previous run of Geth yet. As + // such, we can't say for sure if a hash is not present for anything. Until + // the init is done, we're faking "possible presence" for everything. + return true + } + // Bloom initialized, check the real one and report any successful misses + maybe := b.bloom.Contains(syncBloomHasher(hash)) + if !maybe { + bloomMissMeter.Mark(1) + } + return maybe +} + +// errorRate calculates the probability of a random containment test returning a +// false positive. +// +// We're calculating it ourselves because the bloom library we used missed a +// parentheses in the formula and calculates it wrong. And it's discontinued... +func (b *SyncBloom) errorRate() float64 { + k := float64(b.bloom.K()) + n := float64(b.bloom.N()) + m := float64(b.bloom.M()) + + return math.Pow(1.0-math.Exp((-k)*(n+0.5)/(m-1)), k) +} diff --git a/trie/sync_test.go b/trie/sync_test.go index 0d8c29cfe7..0621bb4357 100644 --- a/trie/sync_test.go +++ b/trie/sync_test.go @@ -94,7 +94,7 @@ func TestEmptySync(t *testing.T) { emptyB, _ := New(emptyRoot, dbB) for i, trie := range []*Trie{emptyA, emptyB} { - if req := NewSync(trie.Hash(), memorydb.New(), nil).Missing(1); len(req) != 0 { + if req := NewSync(trie.Hash(), memorydb.New(), nil, NewSyncBloom(1, memorydb.New())).Missing(1); len(req) != 0 { t.Errorf("test %d: content requested for empty trie: %v", i, req) } } @@ -112,7 +112,7 @@ func testIterativeSync(t *testing.T, batch int) { // Create a destination trie and sync with the scheduler diskdb := memorydb.New() triedb := NewDatabase(diskdb) - sched := NewSync(srcTrie.Hash(), diskdb, nil) + sched := NewSync(srcTrie.Hash(), diskdb, nil, NewSyncBloom(1, diskdb)) queue := append([]common.Hash{}, sched.Missing(batch)...) for len(queue) > 0 { @@ -145,7 +145,7 @@ func TestIterativeDelayedSync(t *testing.T) { // Create a destination trie and sync with the scheduler diskdb := memorydb.New() triedb := NewDatabase(diskdb) - sched := NewSync(srcTrie.Hash(), diskdb, nil) + sched := NewSync(srcTrie.Hash(), diskdb, nil, NewSyncBloom(1, diskdb)) queue := append([]common.Hash{}, sched.Missing(10000)...) for len(queue) > 0 { @@ -183,7 +183,7 @@ func testIterativeRandomSync(t *testing.T, batch int) { // Create a destination trie and sync with the scheduler diskdb := memorydb.New() triedb := NewDatabase(diskdb) - sched := NewSync(srcTrie.Hash(), diskdb, nil) + sched := NewSync(srcTrie.Hash(), diskdb, nil, NewSyncBloom(1, diskdb)) queue := make(map[common.Hash]struct{}) for _, hash := range sched.Missing(batch) { @@ -224,7 +224,7 @@ func TestIterativeRandomDelayedSync(t *testing.T) { // Create a destination trie and sync with the scheduler diskdb := memorydb.New() triedb := NewDatabase(diskdb) - sched := NewSync(srcTrie.Hash(), diskdb, nil) + sched := NewSync(srcTrie.Hash(), diskdb, nil, NewSyncBloom(1, diskdb)) queue := make(map[common.Hash]struct{}) for _, hash := range sched.Missing(10000) { @@ -271,7 +271,7 @@ func TestDuplicateAvoidanceSync(t *testing.T) { // Create a destination trie and sync with the scheduler diskdb := memorydb.New() triedb := NewDatabase(diskdb) - sched := NewSync(srcTrie.Hash(), diskdb, nil) + sched := NewSync(srcTrie.Hash(), diskdb, nil, NewSyncBloom(1, diskdb)) queue := append([]common.Hash{}, sched.Missing(0)...) requested := make(map[common.Hash]struct{}) @@ -311,7 +311,7 @@ func TestIncompleteSync(t *testing.T) { // Create a destination trie and sync with the scheduler diskdb := memorydb.New() triedb := NewDatabase(diskdb) - sched := NewSync(srcTrie.Hash(), diskdb, nil) + sched := NewSync(srcTrie.Hash(), diskdb, nil, NewSyncBloom(1, diskdb)) var added []common.Hash queue := append([]common.Hash{}, sched.Missing(1)...) diff --git a/vendor/github.com/steakknife/bloomfilter/MIT-LICENSE.txt b/vendor/github.com/steakknife/bloomfilter/MIT-LICENSE.txt new file mode 100644 index 0000000000..ccf77fe465 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/MIT-LICENSE.txt @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright © 2014, 2015 Barry Allard + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/steakknife/bloomfilter/README.md b/vendor/github.com/steakknife/bloomfilter/README.md new file mode 100644 index 0000000000..4587f143af --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/README.md @@ -0,0 +1,123 @@ +**Important**: Zeroth, [consider](https://bdupras.github.io/filter-tutorial/) if a [Cuckoo filter](https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf) could be [right for your use-case](https://github.com/seiflotfy/cuckoofilter). + + +[![GoDoc](https://godoc.org/github.com/steakknife/bloomfilter?status.png)](https://godoc.org/github.com/steakknife/bloomfilter) [![travis](https://img.shields.io/travis/steakknife/bloomfilter.svg)](https://travis-ci.org/steakknife/bloomfilter) + +# Face-meltingly fast, thread-safe, marshalable, unionable, probability- and optimal-size-calculating Bloom filter in go + +Copyright © 2014-2016,2018 Barry Allard + +[MIT license](MIT-LICENSE.txt) + +## WTF is a bloom filter + +**TL;DR: **Probabilistic, extra lookup table to track a set of elements kept elsewhere to reduce expensive, unnecessary set element retrieval and/or iterator operations **when an element is not present in the set.** It's a classic time-storage tradeoff algoritm. + +### Properties + +#### [See wikipedia](https://en.wikipedia.org/wiki/Bloom_filter) for algorithm details + +|Impact|What|Description| +|---|---|---| +|Good|No false negatives|know for certain if a given element is definitely NOT in the set| +|Bad|False positives|uncertain if a given element is in the set| +|Bad|Theoretical potential for hash collisions|in very large systems and/or badly hash.Hash64-conforming implementations| +|Bad|Add only|Cannot remove an element, it would destroy information about other elements| +|Good|Constant storage|uses only a fixed amount of memory| + +## Naming conventions + +(Similar to algorithm) + +|Variable/function|Description|Range| +|---|---|---| +|m/M()|number of bits in the bloom filter (memory representation is about m/8 bytes in size)|>=2| +|n/N()|number of elements present|>=0| +|k/K()|number of keys to use (keys are kept private to user code but are de/serialized to Marshal and file I/O)|>=0| +|maxN|maximum capacity of intended structure|>0| +|p|maximum allowed probability of collision (for computing m and k for optimal sizing)|>0..<1| + +- Memory representation should be exactly `24 + 8*(k + (m+63)/64) + unsafe.Sizeof(RWMutex)` bytes. +- Serialized (`BinaryMarshaler`) representation should be exactly `72 + 8*(k + (m+63)/64)` bytes. (Disk format is less due to compression.) + +## Binary serialization format + +All values in Little-endian format + +|Offset|Offset (Hex)|Length (bytes)|Name|Type| +|---|---|---|---|---| +|0|00|8|k|`uint64`| +|8|08|8|n|`uint64`| +|16|10|8|m|`uint64`| +|24|18|k|(keys)|`[k]uint64`| +|24+8*k|...|(m+63)/64|(bloom filter)|`[(m+63)/64]uint64`| +|24+8\*k+8\*((m+63)/64)|...|48|(SHA384 of all previous fields, hashed in order)|`[48]byte`| + +- `bloomfilter.Filter` conforms to `encoding.BinaryMarshaler` and `encoding.BinaryUnmarshaler' + +## Usage + +```go + +import "github.com/steakknife/bloomfilter" + +const ( + maxElements = 100000 + probCollide = 0.0000001 +) + +bf, err := bloomfilter.NewOptimal(maxElements, probCollide) +if err != nil { + panic(err) +} + +someValue := ... // must conform to hash.Hash64 + +bf.Add(someValue) +if bf.Contains(someValue) { // probably true, could be false + // whatever +} + +anotherValue := ... // must also conform to hash.Hash64 + +if bf.Contains(anotherValue) { + panic("This should never happen") +} + +err := bf.WriteFile("1.bf.gz") // saves this BF to a file +if err != nil { + panic(err) +} + +bf2, err := bloomfilter.ReadFile("1.bf.gz") // read the BF to another var +if err != nil { + panic(err) +} +``` + + +## Design + +Where possible, branch-free operations are used to avoid deep pipeline / execution unit stalls on branch-misses. + +## Get + + go get -u github.com/steakknife/bloomfilter # master is always stable + +## Source + +- On the web: [https://github.com/steakknife/bloomfilter](https://github.com/steakknife/bloomfilter) + +- Git: `git clone https://github.com/steakknife/bloomfilter` + +## Contact + +- [Feedback](mailto:barry.allard@gmail.com) + +- [Issues](https://github.com/steakknife/bloomfilter/issues) + +## License + +[MIT license](MIT-LICENSE.txt) + +Copyright © 2014-2016 Barry Allard diff --git a/vendor/github.com/steakknife/bloomfilter/binarymarshaler.go b/vendor/github.com/steakknife/bloomfilter/binarymarshaler.go new file mode 100644 index 0000000000..2fa669254a --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/binarymarshaler.go @@ -0,0 +1,87 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "bytes" + "crypto/sha512" + "encoding/binary" +) + +// conforms to encoding.BinaryMarshaler + +// marshalled binary layout (Little Endian): +// +// k 1 uint64 +// n 1 uint64 +// m 1 uint64 +// keys [k]uint64 +// bits [(m+63)/64]uint64 +// hash sha384 (384 bits == 48 bytes) +// +// size = (3 + k + (m+63)/64) * 8 bytes +// + +func (f *Filter) marshal() (buf *bytes.Buffer, + hash [sha512.Size384]byte, + err error, +) { + f.lock.RLock() + defer f.lock.RUnlock() + + debug("write bf k=%d n=%d m=%d\n", f.K(), f.n, f.m) + + buf = new(bytes.Buffer) + + err = binary.Write(buf, binary.LittleEndian, f.K()) + if err != nil { + return nil, hash, err + } + + err = binary.Write(buf, binary.LittleEndian, f.n) + if err != nil { + return nil, hash, err + } + + err = binary.Write(buf, binary.LittleEndian, f.m) + if err != nil { + return nil, hash, err + } + + err = binary.Write(buf, binary.LittleEndian, f.keys) + if err != nil { + return nil, hash, err + } + + err = binary.Write(buf, binary.LittleEndian, f.bits) + if err != nil { + return nil, hash, err + } + + hash = sha512.Sum384(buf.Bytes()) + err = binary.Write(buf, binary.LittleEndian, hash) + return buf, hash, err +} + +// MarshalBinary converts a Filter into []bytes +func (f *Filter) MarshalBinary() (data []byte, err error) { + buf, hash, err := f.marshal() + if err != nil { + return nil, err + } + + debug( + "bloomfilter.MarshalBinary: Successfully wrote %d byte(s), sha384 %v", + buf.Len(), hash, + ) + data = buf.Bytes() + return data, nil +} diff --git a/vendor/github.com/steakknife/bloomfilter/binaryunmarshaler.go b/vendor/github.com/steakknife/bloomfilter/binaryunmarshaler.go new file mode 100644 index 0000000000..5be1670c58 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/binaryunmarshaler.go @@ -0,0 +1,111 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/binary" + "io" +) + +func unmarshalBinaryHeader(r io.Reader) (k, n, m uint64, err error) { + err = binary.Read(r, binary.LittleEndian, &k) + if err != nil { + return k, n, m, err + } + + if k < KMin { + return k, n, m, errK() + } + + err = binary.Read(r, binary.LittleEndian, &n) + if err != nil { + return k, n, m, err + } + + err = binary.Read(r, binary.LittleEndian, &m) + if err != nil { + return k, n, m, err + } + + if m < MMin { + return k, n, m, errM() + } + + debug("read bf k=%d n=%d m=%d\n", k, n, m) + + return k, n, m, err +} + +func unmarshalBinaryBits(r io.Reader, m uint64) (bits []uint64, err error) { + bits, err = newBits(m) + if err != nil { + return bits, err + } + err = binary.Read(r, binary.LittleEndian, bits) + return bits, err + +} + +func unmarshalBinaryKeys(r io.Reader, k uint64) (keys []uint64, err error) { + keys = make([]uint64, k) + err = binary.Read(r, binary.LittleEndian, keys) + return keys, err +} + +func checkBinaryHash(r io.Reader, data []byte) (err error) { + expectedHash := make([]byte, sha512.Size384) + err = binary.Read(r, binary.LittleEndian, expectedHash) + if err != nil { + return err + } + + actualHash := sha512.Sum384(data[:len(data)-sha512.Size384]) + + if !hmac.Equal(expectedHash, actualHash[:]) { + debug("bloomfilter.UnmarshalBinary() sha384 hash failed:"+ + " actual %v expected %v", actualHash, expectedHash) + return errHash() + } + + debug("bloomfilter.UnmarshalBinary() successfully read"+ + " %d byte(s), sha384 %v", len(data), actualHash) + return nil +} + +// UnmarshalBinary converts []bytes into a Filter +// conforms to encoding.BinaryUnmarshaler +func (f *Filter) UnmarshalBinary(data []byte) (err error) { + f.lock.Lock() + defer f.lock.Unlock() + + buf := bytes.NewBuffer(data) + + var k uint64 + k, f.n, f.m, err = unmarshalBinaryHeader(buf) + if err != nil { + return err + } + + f.keys, err = unmarshalBinaryKeys(buf, k) + if err != nil { + return err + } + + f.bits, err = unmarshalBinaryBits(buf, f.m) + if err != nil { + return err + } + + return checkBinaryHash(buf, data) +} diff --git a/vendor/github.com/steakknife/bloomfilter/bloomfilter.go b/vendor/github.com/steakknife/bloomfilter/bloomfilter.go new file mode 100644 index 0000000000..8225063653 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/bloomfilter.go @@ -0,0 +1,123 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "hash" + "sync" +) + +// Filter is an opaque Bloom filter type +type Filter struct { + lock sync.RWMutex + bits []uint64 + keys []uint64 + m uint64 // number of bits the "bits" field should recognize + n uint64 // number of inserted elements +} + +// Hashable -> hashes +func (f *Filter) hash(v hash.Hash64) []uint64 { + rawHash := v.Sum64() + n := len(f.keys) + hashes := make([]uint64, n) + for i := 0; i < n; i++ { + hashes[i] = rawHash ^ f.keys[i] + } + return hashes +} + +// M is the size of Bloom filter, in bits +func (f *Filter) M() uint64 { + return f.m +} + +// K is the count of keys +func (f *Filter) K() uint64 { + return uint64(len(f.keys)) +} + +// Add a hashable item, v, to the filter +func (f *Filter) Add(v hash.Hash64) { + f.lock.Lock() + defer f.lock.Unlock() + + for _, i := range f.hash(v) { + // f.setBit(i) + i %= f.m + f.bits[i>>6] |= 1 << uint(i&0x3f) + } + f.n++ +} + +// Contains tests if f contains v +// false: f definitely does not contain value v +// true: f maybe contains value v +func (f *Filter) Contains(v hash.Hash64) bool { + f.lock.RLock() + defer f.lock.RUnlock() + + r := uint64(1) + for _, i := range f.hash(v) { + // r |= f.getBit(k) + i %= f.m + r &= (f.bits[i>>6] >> uint(i&0x3f)) & 1 + } + return uint64ToBool(r) +} + +// Copy f to a new Bloom filter +func (f *Filter) Copy() (*Filter, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + out, err := f.NewCompatible() + if err != nil { + return nil, err + } + copy(out.bits, f.bits) + out.n = f.n + return out, nil +} + +// UnionInPlace merges Bloom filter f2 into f +func (f *Filter) UnionInPlace(f2 *Filter) error { + if !f.IsCompatible(f2) { + return errIncompatibleBloomFilters() + } + + f.lock.Lock() + defer f.lock.Unlock() + + for i, bitword := range f2.bits { + f.bits[i] |= bitword + } + return nil +} + +// Union merges f2 and f2 into a new Filter out +func (f *Filter) Union(f2 *Filter) (out *Filter, err error) { + if !f.IsCompatible(f2) { + return nil, errIncompatibleBloomFilters() + } + + f.lock.RLock() + defer f.lock.RUnlock() + + out, err = f.NewCompatible() + if err != nil { + return nil, err + } + for i, bitword := range f2.bits { + out.bits[i] = f.bits[i] | bitword + } + return out, nil +} diff --git a/vendor/github.com/steakknife/bloomfilter/conformance.go b/vendor/github.com/steakknife/bloomfilter/conformance.go new file mode 100644 index 0000000000..2963686f85 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/conformance.go @@ -0,0 +1,29 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "encoding" + "encoding/gob" + "io" +) + +// compile-time conformance tests +var ( + _ encoding.BinaryMarshaler = (*Filter)(nil) + _ encoding.BinaryUnmarshaler = (*Filter)(nil) + _ encoding.TextMarshaler = (*Filter)(nil) + _ encoding.TextUnmarshaler = (*Filter)(nil) + _ io.ReaderFrom = (*Filter)(nil) + _ io.WriterTo = (*Filter)(nil) + _ gob.GobDecoder = (*Filter)(nil) + _ gob.GobEncoder = (*Filter)(nil) +) diff --git a/vendor/github.com/steakknife/bloomfilter/debug.go b/vendor/github.com/steakknife/bloomfilter/debug.go new file mode 100644 index 0000000000..e88b934c17 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/debug.go @@ -0,0 +1,37 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "log" + "os" +) + +const debugVar = "GOLANG_STEAKKNIFE_BLOOMFILTER_DEBUG" + +// EnableDebugging permits debug() logging of details to stderr +func EnableDebugging() { + err := os.Setenv(debugVar, "1") + if err != nil { + panic("Unable to Setenv " + debugVar) + } +} + +func debugging() bool { + return os.Getenv(debugVar) != "" +} + +// debug printing when debugging() is true +func debug(format string, a ...interface{}) { + if debugging() { + log.Printf(format, a...) + } +} diff --git a/vendor/github.com/steakknife/bloomfilter/errors.go b/vendor/github.com/steakknife/bloomfilter/errors.go new file mode 100644 index 0000000000..b279739b36 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/errors.go @@ -0,0 +1,34 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import "fmt" + +func errHash() error { + return fmt.Errorf( + "Hash mismatch, the Bloom filter is probably corrupt") +} +func errK() error { + return fmt.Errorf( + "keys must have length %d or greater", KMin) +} +func errM() error { + return fmt.Errorf( + "m (number of bits in the Bloom filter) must be >= %d", MMin) +} +func errUniqueKeys() error { + return fmt.Errorf( + "Bloom filter keys must be unique") +} +func errIncompatibleBloomFilters() error { + return fmt.Errorf( + "Cannot perform union on two incompatible Bloom filters") +} diff --git a/vendor/github.com/steakknife/bloomfilter/fileio.go b/vendor/github.com/steakknife/bloomfilter/fileio.go new file mode 100644 index 0000000000..a479699566 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/fileio.go @@ -0,0 +1,105 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "compress/gzip" + "io" + "io/ioutil" + "os" +) + +// ReadFrom r and overwrite f with new Bloom filter data +func (f *Filter) ReadFrom(r io.Reader) (n int64, err error) { + f2, n, err := ReadFrom(r) + if err != nil { + return -1, err + } + f.lock.Lock() + defer f.lock.Unlock() + f.m = f2.m + f.n = f2.n + f.bits = f2.bits + f.keys = f2.keys + return n, nil +} + +// ReadFrom Reader r into a lossless-compressed Bloom filter f +func ReadFrom(r io.Reader) (f *Filter, n int64, err error) { + rawR, err := gzip.NewReader(r) + if err != nil { + return nil, -1, err + } + defer func() { + err = rawR.Close() + }() + + content, err := ioutil.ReadAll(rawR) + if err != nil { + return nil, -1, err + } + + f = new(Filter) + n = int64(len(content)) + err = f.UnmarshalBinary(content) + if err != nil { + return nil, -1, err + } + return f, n, nil +} + +// ReadFile from filename into a lossless-compressed Bloom Filter f +// Suggested file extension: .bf.gz +func ReadFile(filename string) (f *Filter, n int64, err error) { + r, err := os.Open(filename) + if err != nil { + return nil, -1, err + } + defer func() { + err = r.Close() + }() + + return ReadFrom(r) +} + +// WriteTo a Writer w from lossless-compressed Bloom Filter f +func (f *Filter) WriteTo(w io.Writer) (n int64, err error) { + f.lock.RLock() + defer f.lock.RUnlock() + + rawW := gzip.NewWriter(w) + defer func() { + err = rawW.Close() + }() + + content, err := f.MarshalBinary() + if err != nil { + return -1, err + } + + intN, err := rawW.Write(content) + n = int64(intN) + return n, err +} + +// WriteFile filename from a a lossless-compressed Bloom Filter f +// Suggested file extension: .bf.gz +func (f *Filter) WriteFile(filename string) (n int64, err error) { + w, err := os.Create(filename) + if err != nil { + return -1, err + } + defer func() { + err = w.Close() + }() + + return f.WriteTo(w) +} diff --git a/vendor/github.com/steakknife/bloomfilter/gob.go b/vendor/github.com/steakknife/bloomfilter/gob.go new file mode 100644 index 0000000000..0d99e55d43 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/gob.go @@ -0,0 +1,23 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import _ "encoding/gob" // make sure gob is available + +// GobDecode conforms to interface gob.GobDecoder +func (f *Filter) GobDecode(data []byte) error { + return f.UnmarshalBinary(data) +} + +// GobEncode conforms to interface gob.GobEncoder +func (f *Filter) GobEncode() ([]byte, error) { + return f.MarshalBinary() +} diff --git a/vendor/github.com/steakknife/bloomfilter/iscompatible.go b/vendor/github.com/steakknife/bloomfilter/iscompatible.go new file mode 100644 index 0000000000..2073d8083e --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/iscompatible.go @@ -0,0 +1,41 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import "unsafe" + +func uint64ToBool(x uint64) bool { + return *(*bool)(unsafe.Pointer(&x)) // #nosec +} + +// returns 0 if equal, does not compare len(b0) with len(b1) +func noBranchCompareUint64s(b0, b1 []uint64) uint64 { + r := uint64(0) + for i, b0i := range b0 { + r |= b0i ^ b1[i] + } + return r +} + +// IsCompatible is true if f and f2 can be Union()ed together +func (f *Filter) IsCompatible(f2 *Filter) bool { + f.lock.RLock() + defer f.lock.RUnlock() + + f.lock.RLock() + defer f2.lock.RUnlock() + + // 0 is true, non-0 is false + compat := f.M() ^ f2.M() + compat |= f.K() ^ f2.K() + compat |= noBranchCompareUint64s(f.keys, f2.keys) + return uint64ToBool(^compat) +} diff --git a/vendor/github.com/steakknife/bloomfilter/new.go b/vendor/github.com/steakknife/bloomfilter/new.go new file mode 100644 index 0000000000..bf4323a7ed --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/new.go @@ -0,0 +1,134 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "crypto/rand" + "encoding/binary" + "log" +) + +const ( + // MMin is the minimum Bloom filter bits count + MMin = 2 + // KMin is the minimum number of keys + KMin = 1 + // Uint64Bytes is the number of bytes in type uint64 + Uint64Bytes = 8 +) + +// New Filter with CSPRNG keys +// +// m is the size of the Bloom filter, in bits, >= 2 +// +// k is the number of random keys, >= 1 +func New(m, k uint64) (*Filter, error) { + return NewWithKeys(m, newRandKeys(k)) +} + +func newRandKeys(k uint64) []uint64 { + keys := make([]uint64, k) + err := binary.Read(rand.Reader, binary.LittleEndian, keys) + if err != nil { + log.Panicf( + "Cannot read %d bytes from CSRPNG crypto/rand.Read (err=%v)", + Uint64Bytes, err, + ) + } + return keys +} + +// NewCompatible Filter compatible with f +func (f *Filter) NewCompatible() (*Filter, error) { + return NewWithKeys(f.m, f.keys) +} + +// NewOptimal Bloom filter with random CSPRNG keys +func NewOptimal(maxN uint64, p float64) (*Filter, error) { + m := OptimalM(maxN, p) + k := OptimalK(m, maxN) + debug("New optimal bloom filter ::"+ + " requested max elements (n):%d,"+ + " probability of collision (p):%1.10f "+ + "-> recommends -> bits (m): %d (%f GiB), "+ + "number of keys (k): %d", + maxN, p, m, float64(m)/(gigabitsPerGiB), k) + return New(m, k) +} + +// UniqueKeys is true if all keys are unique +func UniqueKeys(keys []uint64) bool { + for j := 0; j < len(keys)-1; j++ { + elem := keys[j] + for i := 1; i < j; i++ { + if keys[i] == elem { + return false + } + } + } + return true +} + +// NewWithKeys creates a new Filter from user-supplied origKeys +func NewWithKeys(m uint64, origKeys []uint64) (f *Filter, err error) { + bits, err := newBits(m) + if err != nil { + return nil, err + } + keys, err := newKeysCopy(origKeys) + if err != nil { + return nil, err + } + return &Filter{ + m: m, + n: 0, + bits: bits, + keys: keys, + }, nil +} + +func newBits(m uint64) ([]uint64, error) { + if m < MMin { + return nil, errM() + } + return make([]uint64, (m+63)/64), nil +} + +func newKeysBlank(k uint64) ([]uint64, error) { + if k < KMin { + return nil, errK() + } + return make([]uint64, k), nil +} + +func newKeysCopy(origKeys []uint64) (keys []uint64, err error) { + if !UniqueKeys(origKeys) { + return nil, errUniqueKeys() + } + keys, err = newKeysBlank(uint64(len(origKeys))) + if err != nil { + return keys, err + } + copy(keys, origKeys) + return keys, err +} + +func newWithKeysAndBits(m uint64, keys []uint64, bits []uint64, n uint64) ( + f *Filter, err error, +) { + f, err = NewWithKeys(m, keys) + if err != nil { + return nil, err + } + copy(f.bits, bits) + f.n = n + return f, nil +} diff --git a/vendor/github.com/steakknife/bloomfilter/optimal.go b/vendor/github.com/steakknife/bloomfilter/optimal.go new file mode 100644 index 0000000000..a8361434a5 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/optimal.go @@ -0,0 +1,28 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import "math" + +const gigabitsPerGiB float64 = 8.0 * 1024 * 1024 * 1024 + +// OptimalK calculates the optimal k value for creating a new Bloom filter +// maxn is the maximum anticipated number of elements +func OptimalK(m, maxN uint64) uint64 { + return uint64(math.Ceil(float64(m) * math.Ln2 / float64(maxN))) +} + +// OptimalM calculates the optimal m value for creating a new Bloom filter +// p is the desired false positive probability +// optimal m = ceiling( - n * ln(p) / ln(2)**2 ) +func OptimalM(maxN uint64, p float64) uint64 { + return uint64(math.Ceil(-float64(maxN) * math.Log(p) / (math.Ln2 * math.Ln2))) +} diff --git a/vendor/github.com/steakknife/bloomfilter/statistics.go b/vendor/github.com/steakknife/bloomfilter/statistics.go new file mode 100644 index 0000000000..fe50ffa544 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/statistics.go @@ -0,0 +1,43 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "math" + + "github.com/steakknife/hamming" +) + +// PreciseFilledRatio is an exhaustive count # of 1's +func (f *Filter) PreciseFilledRatio() float64 { + f.lock.RLock() + defer f.lock.RUnlock() + + return float64(hamming.CountBitsUint64s(f.bits)) / float64(f.M()) +} + +// N is how many elements have been inserted +// (actually, how many Add()s have been performed?) +func (f *Filter) N() uint64 { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.n +} + +// FalsePosititveProbability is the upper-bound probability of false positives +// (1 - exp(-k*(n+0.5)/(m-1))) ** k +func (f *Filter) FalsePosititveProbability() float64 { + k := float64(f.K()) + n := float64(f.N()) + m := float64(f.M()) + return math.Pow(1.0-math.Exp(-k)*(n+0.5)/(m-1), k) +} diff --git a/vendor/github.com/steakknife/bloomfilter/textmarshaler.go b/vendor/github.com/steakknife/bloomfilter/textmarshaler.go new file mode 100644 index 0000000000..7ed08eb7f9 --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/textmarshaler.go @@ -0,0 +1,49 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import "fmt" + +// MarshalText conforms to encoding.TextMarshaler +func (f *Filter) MarshalText() (text []byte, err error) { + f.lock.RLock() + defer f.lock.RUnlock() + + s := fmt.Sprintln("k") + s += fmt.Sprintln(f.K()) + s += fmt.Sprintln("n") + s += fmt.Sprintln(f.n) + s += fmt.Sprintln("m") + s += fmt.Sprintln(f.m) + + s += fmt.Sprintln("keys") + for key := range f.keys { + s += fmt.Sprintf(keyFormat, key) + nl() + } + + s += fmt.Sprintln("bits") + for w := range f.bits { + s += fmt.Sprintf(bitsFormat, w) + nl() + } + + _, hash, err := f.marshal() + if err != nil { + return nil, err + } + s += fmt.Sprintln("sha384") + for b := range hash { + s += fmt.Sprintf("%02x", b) + } + s += nl() + + text = []byte(s) + return text, nil +} diff --git a/vendor/github.com/steakknife/bloomfilter/textunmarshaler.go b/vendor/github.com/steakknife/bloomfilter/textunmarshaler.go new file mode 100644 index 0000000000..93240a1a7d --- /dev/null +++ b/vendor/github.com/steakknife/bloomfilter/textunmarshaler.go @@ -0,0 +1,150 @@ +// Package bloomfilter is face-meltingly fast, thread-safe, +// marshalable, unionable, probability- and +// optimal-size-calculating Bloom filter in go +// +// https://github.com/steakknife/bloomfilter +// +// Copyright © 2014, 2015, 2018 Barry Allard +// +// MIT license +// +package bloomfilter + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "fmt" + "io" +) + +const ( + keyFormat = "%016x" + bitsFormat = "%016x" +) + +func nl() string { + return fmt.Sprintln() +} + +func unmarshalTextHeader(r io.Reader) (k, n, m uint64, err error) { + format := "k" + nl() + "%d" + nl() + format += "n" + nl() + "%d" + nl() + format += "m" + nl() + "%d" + nl() + format += "keys" + nl() + + _, err = fmt.Fscanf(r, format, k, n, m) + return k, n, m, err +} + +func unmarshalTextKeys(r io.Reader, keys []uint64) (err error) { + for i := range keys { + _, err = fmt.Fscanf(r, keyFormat, keys[i]) + if err != nil { + return err + } + } + return nil +} + +func unmarshalTextBits(r io.Reader, bits []uint64) (err error) { + _, err = fmt.Fscanf(r, "bits") + if err != nil { + return err + } + + for i := range bits { + _, err = fmt.Fscanf(r, bitsFormat, bits[i]) + if err != nil { + return err + } + } + + return nil +} + +func unmarshalAndCheckTextHash(r io.Reader, f *Filter) (err error) { + _, err = fmt.Fscanf(r, "sha384") + if err != nil { + return err + } + + actualHash := [sha512.Size384]byte{} + + for i := range actualHash { + _, err = fmt.Fscanf(r, "%02x", actualHash[i]) + if err != nil { + return err + } + } + + _, expectedHash, err := f.marshal() + if err != nil { + return err + } + + if !hmac.Equal(expectedHash[:], actualHash[:]) { + return errHash() + } + + return nil +} + +// UnmarshalText conforms to TextUnmarshaler +func UnmarshalText(text []byte) (f *Filter, err error) { + r := bytes.NewBuffer(text) + k, n, m, err := unmarshalTextHeader(r) + if err != nil { + return nil, err + } + + keys, err := newKeysBlank(k) + if err != nil { + return nil, err + } + + err = unmarshalTextKeys(r, keys) + if err != nil { + return nil, err + } + + bits, err := newBits(m) + if err != nil { + return nil, err + } + + err = unmarshalTextBits(r, bits) + if err != nil { + return nil, err + } + + f, err = newWithKeysAndBits(m, keys, bits, n) + if err != nil { + return nil, err + } + + err = unmarshalAndCheckTextHash(r, f) + if err != nil { + return nil, err + } + + return f, nil +} + +// UnmarshalText method overwrites f with data decoded from text +func (f *Filter) UnmarshalText(text []byte) error { + f.lock.Lock() + defer f.lock.Unlock() + + f2, err := UnmarshalText(text) + if err != nil { + return err + } + + f.m = f2.m + f.n = f2.n + copy(f.bits, f2.bits) + copy(f.keys, f2.keys) + + return nil +} diff --git a/vendor/github.com/steakknife/hamming/MIT-LICENSE.txt b/vendor/github.com/steakknife/hamming/MIT-LICENSE.txt new file mode 100644 index 0000000000..924f4c0918 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/MIT-LICENSE.txt @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright © 2014, 2015, 2016 Barry Allard + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/steakknife/hamming/README.md b/vendor/github.com/steakknife/hamming/README.md new file mode 100644 index 0000000000..23f69bdf5e --- /dev/null +++ b/vendor/github.com/steakknife/hamming/README.md @@ -0,0 +1,82 @@ +[![GoDoc](https://godoc.org/github.com/steakknife/hamming?status.png)](https://godoc.org/github.com/steakknife/hamming) [![Build Status](https://travis-ci.org/steakknife/hamming.svg?branch=master)](https://travis-ci.org/steakknife/hamming) + + +# hamming distance calculations in Go + +Copyright © 2014, 2015, 2016, 2018 Barry Allard + +[MIT license](MIT-LICENSE.txt) + +## Performance + +``` +$ go test -bench=. +BenchmarkCountBitsInt8PopCnt-4 300000000 4.30 ns/op +BenchmarkCountBitsInt16PopCnt-4 300000000 3.83 ns/op +BenchmarkCountBitsInt32PopCnt-4 300000000 3.64 ns/op +BenchmarkCountBitsInt64PopCnt-4 500000000 3.60 ns/op +BenchmarkCountBitsIntPopCnt-4 300000000 5.72 ns/op +BenchmarkCountBitsUint8PopCnt-4 1000000000 2.98 ns/op +BenchmarkCountBitsUint16PopCnt-4 500000000 3.23 ns/op +BenchmarkCountBitsUint32PopCnt-4 500000000 3.00 ns/op +BenchmarkCountBitsUint64PopCnt-4 1000000000 2.94 ns/op +BenchmarkCountBitsUintPopCnt-4 300000000 5.04 ns/op +BenchmarkCountBitsBytePopCnt-4 300000000 3.99 ns/op +BenchmarkCountBitsRunePopCnt-4 300000000 3.83 ns/op +BenchmarkCountBitsInt8-4 2000000000 0.74 ns/op +BenchmarkCountBitsInt16-4 2000000000 1.54 ns/op +BenchmarkCountBitsInt32-4 1000000000 2.63 ns/op +BenchmarkCountBitsInt64-4 1000000000 2.56 ns/op +BenchmarkCountBitsInt-4 200000000 7.23 ns/op +BenchmarkCountBitsUint16-4 2000000000 1.51 ns/op +BenchmarkCountBitsUint32-4 500000000 4.00 ns/op +BenchmarkCountBitsUint64-4 1000000000 2.64 ns/op +BenchmarkCountBitsUint64Alt-4 200000000 7.60 ns/op +BenchmarkCountBitsUint-4 300000000 5.48 ns/op +BenchmarkCountBitsUintReference-4 100000000 19.2 ns/op +BenchmarkCountBitsByte-4 2000000000 0.75 ns/op +BenchmarkCountBitsByteAlt-4 1000000000 2.37 ns/op +BenchmarkCountBitsRune-4 500000000 2.85 ns/op +PASS +ok _/Users/bmf/Projects/hamming 58.305s +$ +``` + +## Usage + +```go +import 'github.com/steakknife/hamming' + +// ... + +// hamming distance between values +hamming.Byte(0xFF, 0x00) // 8 +hamming.Byte(0x00, 0x00) // 0 + +// just count bits in a byte +hamming.CountBitsByte(0xA5), // 4 +``` + +See help in the [docs](https://godoc.org/github.com/steakknife/hamming) + +## Get + + go get -u github.com/steakknife/hamming # master is always stable + +## Source + +- On the web: https://github.com/steakknife/hamming + +- Git: `git clone https://github.com/steakknife/hamming` + +## Contact + +- [Feedback](mailto:barry.allard@gmail.com) + +- [Issues](https://github.com/steakknife/hamming/issues) + +## License + +[MIT license](MIT-LICENSE.txt) + +Copyright © 2014, 2015, 2016 Barry Allard diff --git a/vendor/github.com/steakknife/hamming/doc.go b/vendor/github.com/steakknife/hamming/doc.go new file mode 100644 index 0000000000..179e29da92 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/doc.go @@ -0,0 +1,35 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +// +// Usage +// +// For functions named CountBits.+s?. The plural forms are for slices. +// The CountBits.+ forms are Population Count only, where the bare-type +// forms are Hamming distance (number of bits different) between two values. +// +// Optimized assembly .+PopCnt forms are available on amd64, and operate just +// like the regular forms (Must check and guard on HasPopCnt() first before +// trying to call .+PopCnt functions). +// +// import 'github.com/steakknife/hamming' +// +// // ... +// +// // hamming distance between values +// hamming.Byte(0xFF, 0x00) // 8 +// hamming.Byte(0x00, 0x00) // 0 +// +// // just count bits in a byte +// hamming.CountBitsByte(0xA5), // 4 +// +// Got rune? use int32 +// Got uint8? use byte +// +package hamming diff --git a/vendor/github.com/steakknife/hamming/hamming.go b/vendor/github.com/steakknife/hamming/hamming.go new file mode 100644 index 0000000000..269e91a4d1 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/hamming.go @@ -0,0 +1,70 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +package hamming + +// Int8 hamming distance of two int8's +func Int8(x, y int8) int { + return CountBitsInt8(x ^ y) +} + +// Int16 hamming distance of two int16's +func Int16(x, y int16) int { + return CountBitsInt16(x ^ y) +} + +// Int32 hamming distance of two int32's +func Int32(x, y int32) int { + return CountBitsInt32(x ^ y) +} + +// Int64 hamming distance of two int64's +func Int64(x, y int64) int { + return CountBitsInt64(x ^ y) +} + +// Int hamming distance of two ints +func Int(x, y int) int { + return CountBitsInt(x ^ y) +} + +// Uint8 hamming distance of two uint8's +func Uint8(x, y uint8) int { + return CountBitsUint8(x ^ y) +} + +// Uint16 hamming distance of two uint16's +func Uint16(x, y uint16) int { + return CountBitsUint16(x ^ y) +} + +// Uint32 hamming distance of two uint32's +func Uint32(x, y uint32) int { + return CountBitsUint32(x ^ y) +} + +// Uint64 hamming distance of two uint64's +func Uint64(x, y uint64) int { + return CountBitsUint64(x ^ y) +} + +// Uint hamming distance of two uint's +func Uint(x, y uint) int { + return CountBitsUint(x ^ y) +} + +// Byte hamming distance of two bytes +func Byte(x, y byte) int { + return CountBitsByte(x ^ y) +} + +// Rune hamming distance of two runes +func Rune(x, y rune) int { + return CountBitsRune(x ^ y) +} diff --git a/vendor/github.com/steakknife/hamming/popcnt_amd64.go b/vendor/github.com/steakknife/hamming/popcnt_amd64.go new file mode 100644 index 0000000000..a1a6d92bde --- /dev/null +++ b/vendor/github.com/steakknife/hamming/popcnt_amd64.go @@ -0,0 +1,65 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +package hamming + +import "strconv" + +// HasPopCnt returns true if *PopCnt functions are callable +func HasPopCnt() (ret bool) + +// CountBitsInt8PopCnt count 1's in x +func CountBitsInt8PopCnt(x int8) (ret int) + +// CountBitsInt16PopCnt count 1's in x +func CountBitsInt16PopCnt(x int16) (ret int) + +// CountBitsInt32PopCnt count 1's in x +func CountBitsInt32PopCnt(x int32) (ret int) + +// CountBitsInt64PopCnt count 1's in x +func CountBitsInt64PopCnt(x int64) (ret int) + +// CountBitsIntPopCnt count 1's in x +func CountBitsIntPopCnt(x int) int { + if strconv.IntSize == 64 { + return CountBitsInt64PopCnt(int64(x)) + } else if strconv.IntSize == 32 { + return CountBitsInt32PopCnt(int32(x)) + } + panic("strconv.IntSize must be 32 or 64") +} + +// CountBitsUint8PopCnt count 1's in x +func CountBitsUint8PopCnt(x uint8) (ret int) + +// CountBitsUint16PopCnt count 1's in x +func CountBitsUint16PopCnt(x uint16) (ret int) + +// CountBitsUint32PopCnt count 1's in x +func CountBitsUint32PopCnt(x uint32) (ret int) + +// CountBitsUint64PopCnt count 1's in x +func CountBitsUint64PopCnt(x uint64) (ret int) + +// CountBitsUintPopCnt count 1's in x +func CountBitsUintPopCnt(x uint) int { + if strconv.IntSize == 64 { + return CountBitsUint64PopCnt(uint64(x)) + } else if strconv.IntSize == 32 { + return CountBitsUint32PopCnt(uint32(x)) + } + panic("strconv.IntSize must be 32 or 64") +} + +// CountBitsBytePopCnt count 1's in x +func CountBitsBytePopCnt(x byte) (ret int) + +// CountBitsRunePopCnt count 1's in x +func CountBitsRunePopCnt(x rune) (ret int) diff --git a/vendor/github.com/steakknife/hamming/popcnt_amd64.s b/vendor/github.com/steakknife/hamming/popcnt_amd64.s new file mode 100644 index 0000000000..51c51248a3 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/popcnt_amd64.s @@ -0,0 +1,64 @@ +// +// hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016 Barry Allard +// +// MIT license +// + +#include "textflag.h" + +TEXT ·CountBitsInt8PopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsBytePopCnt(SB) + +TEXT ·CountBitsInt16PopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint16PopCnt(SB) + +TEXT ·CountBitsInt32PopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint32PopCnt(SB) + +TEXT ·CountBitsInt64PopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint64PopCnt(SB) + +TEXT ·CountBitsBytePopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint8PopCnt(SB) + +TEXT ·CountBitsRunePopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint32PopCnt(SB) + +TEXT ·CountBitsUint8PopCnt(SB),NOSPLIT,$0 + XORQ AX, AX + MOVB x+0(FP), AX + POPCNTQ AX, AX + MOVQ AX, ret+8(FP) + RET + +TEXT ·CountBitsUint16PopCnt(SB),NOSPLIT,$0 + XORQ AX, AX + MOVW x+0(FP), AX + POPCNTQ AX, AX + MOVQ AX, ret+8(FP) + RET + +TEXT ·CountBitsUint32PopCnt(SB),NOSPLIT,$0 + XORQ AX, AX + MOVL x+0(FP), AX + POPCNTQ AX, AX + MOVQ AX, ret+8(FP) + RET + +TEXT ·CountBitsUint64PopCnt(SB),NOSPLIT,$0 + POPCNTQ x+0(FP), AX + MOVQ AX, ret+8(FP) + RET + +// func hasPopCnt() (ret bool) +TEXT ·HasPopCnt(SB),NOSPLIT,$0 + MOVL $1, AX + CPUID + SHRL $23, CX // bit 23: Advanced Bit Manipulation Bit (ABM) -> POPCNTQ + ANDL $1, CX + MOVB CX, ret+0(FP) + RET diff --git a/vendor/github.com/steakknife/hamming/popcount.go b/vendor/github.com/steakknife/hamming/popcount.go new file mode 100644 index 0000000000..848103bbf5 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/popcount.go @@ -0,0 +1,134 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +package hamming + +import "strconv" + +// References: check out Hacker's Delight, about p. 70 + +func table() [256]uint8 { + return [256]uint8{ + 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, + } +} + +// CountBitsByteAlt table-less, branch-free implementation +func CountBitsByteAlt(x byte) int { + x = (x & 0x55) + ((x >> 1) & 0x55) + x = (x & 0x33) + ((x >> 2) & 0x33) + return int((x & 0x0f) + ((x >> 4) & 0x0f)) +} + +// CountBitsInt8 count 1's in x +func CountBitsInt8(x int8) int { return CountBitsByte(byte(x)) } + +// CountBitsInt16 count 1's in x +func CountBitsInt16(x int16) int { return CountBitsUint16(uint16(x)) } + +// CountBitsInt32 count 1's in x +func CountBitsInt32(x int32) int { return CountBitsUint32(uint32(x)) } + +// CountBitsInt64 count 1's in x +func CountBitsInt64(x int64) int { return CountBitsUint64(uint64(x)) } + +// CountBitsInt count 1's in x +func CountBitsInt(x int) int { return CountBitsUint(uint(x)) } + +// CountBitsByte count 1's in x +func CountBitsByte(x byte) int { return CountBitsUint8(x) } + +// CountBitsRune count 1's in x +func CountBitsRune(x rune) int { return CountBitsInt32(x) } + +// CountBitsUint8 count 1's in x +func CountBitsUint8(x uint8) int { return int(table()[x]) } + +// CountBitsUint16 count 1's in x +func CountBitsUint16(x uint16) int { + return int(table()[x&0xFF] + table()[(x>>8)&0xFF]) +} + +const ( + m1d uint32 = 0x55555555 + m2d = 0x33333333 + m4d = 0x0f0f0f0f +) + +// CountBitsUint32 count 1's in x +func CountBitsUint32(x uint32) int { + x -= ((x >> 1) & m1d) + x = (x & m2d) + ((x >> 2) & m2d) + x = (x + (x >> 4)) & m4d + x += x >> 8 + x += x >> 16 + return int(x & 0x3f) +} + +const ( + m1q uint64 = 0x5555555555555555 + m2q = 0x3333333333333333 + m4q = 0x0f0f0f0f0f0f0f0f + hq = 0x0101010101010101 +) + +// CountBitsUint64 count 1's in x +func CountBitsUint64(x uint64) int { + // put count of each 2 bits into those 2 bits + x -= (x >> 1) & m1q + + // put count of each 4 bits into those 4 bits + x = (x & m2q) + ((x >> 2) & m2q) + + // put count of each 8 bits into those 8 bits + x = (x + (x >> 4)) & m4q + + // returns left 8 bits of x + (x<<8) + (x<<16) + (x<<24) + ... + return int((x * hq) >> 56) +} + +// CountBitsUint64Alt count 1's in x +func CountBitsUint64Alt(x uint64) int { + return CountBitsUint32(uint32(x>>32)) + CountBitsUint32(uint32(x)) +} + +// CountBitsUintReference count 1's in x +func CountBitsUintReference(x uint) int { + c := 0 + for x != 0 { + x &= x - 1 + c++ + } + return c +} + +// CountBitsUint count 1's in x +func CountBitsUint(x uint) int { + if strconv.IntSize == 64 { + return CountBitsUint64(uint64(x)) + } else if strconv.IntSize == 32 { + return CountBitsUint32(uint32(x)) + } + panic("strconv.IntSize must be 32 or 64 bits") +} diff --git a/vendor/github.com/steakknife/hamming/popcount_slices.go b/vendor/github.com/steakknife/hamming/popcount_slices.go new file mode 100644 index 0000000000..957fe11653 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/popcount_slices.go @@ -0,0 +1,123 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +package hamming + +// CountBitsInt8s count 1's in b +func CountBitsInt8s(b []int8) int { + c := 0 + for _, x := range b { + c += CountBitsInt8(x) + } + return c +} + +// CountBitsInt16s count 1's in b +func CountBitsInt16s(b []int16) int { + c := 0 + for _, x := range b { + c += CountBitsInt16(x) + } + return c +} + +// CountBitsInt32s count 1's in b +func CountBitsInt32s(b []int32) int { + c := 0 + for _, x := range b { + c += CountBitsInt32(x) + } + return c +} + +// CountBitsInt64s count 1's in b +func CountBitsInt64s(b []int64) int { + c := 0 + for _, x := range b { + c += CountBitsInt64(x) + } + return c +} + +// CountBitsInts count 1's in b +func CountBitsInts(b []int) int { + c := 0 + for _, x := range b { + c += CountBitsInt(x) + } + return c +} + +// CountBitsUint8s count 1's in b +func CountBitsUint8s(b []uint8) int { + c := 0 + for _, x := range b { + c += CountBitsUint8(x) + } + return c +} + +// CountBitsUint16s count 1's in b +func CountBitsUint16s(b []uint16) int { + c := 0 + for _, x := range b { + c += CountBitsUint16(x) + } + return c +} + +// CountBitsUint32s count 1's in b +func CountBitsUint32s(b []uint32) int { + c := 0 + for _, x := range b { + c += CountBitsUint32(x) + } + return c +} + +// CountBitsUint64s count 1's in b +func CountBitsUint64s(b []uint64) int { + c := 0 + for _, x := range b { + c += CountBitsUint64(x) + } + return c +} + +// CountBitsUints count 1's in b +func CountBitsUints(b []uint) int { + c := 0 + for _, x := range b { + c += CountBitsUint(x) + } + return c +} + +// CountBitsBytes count 1's in b +func CountBitsBytes(b []byte) int { + c := 0 + for _, x := range b { + c += CountBitsByte(x) + } + return c +} + +// CountBitsRunes count 1's in b +func CountBitsRunes(b []rune) int { + c := 0 + for _, x := range b { + c += CountBitsRune(x) + } + return c +} + +// CountBitsString count 1's in s +func CountBitsString(s string) int { + return CountBitsBytes([]byte(s)) +} diff --git a/vendor/github.com/steakknife/hamming/popcount_slices_amd64.go b/vendor/github.com/steakknife/hamming/popcount_slices_amd64.go new file mode 100644 index 0000000000..b3e13fdf94 --- /dev/null +++ b/vendor/github.com/steakknife/hamming/popcount_slices_amd64.go @@ -0,0 +1,72 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +package hamming + +import ( + "strconv" + "unsafe" +) + +// CountBitsInt8sPopCnt count 1's in x +func CountBitsInt8sPopCnt(x []int8) (ret int) + +// CountBitsInt16sPopCnt count 1's in x +func CountBitsInt16sPopCnt(x []int16) (ret int) + +// CountBitsInt32sPopCnt count 1's in x +func CountBitsInt32sPopCnt(x []int32) (ret int) + +// CountBitsInt64sPopCnt count 1's in x +func CountBitsInt64sPopCnt(x []int64) (ret int) + +// CountBitsIntsPopCnt count 1's in x +func CountBitsIntsPopCnt(x []int) int { + if strconv.IntSize == 64 { + y := (*[]int64)(unsafe.Pointer(&x)) // #nosec G103 + return CountBitsInt64sPopCnt(*y) + } else if strconv.IntSize == 32 { + y := (*[]int32)(unsafe.Pointer(&x)) // #nosec G103 + return CountBitsInt32sPopCnt(*y) + } + panic("strconv.IntSize must be 32 or 64 bits") +} + +// CountBitsUint8sPopCnt count 1's in x +func CountBitsUint8sPopCnt(x []uint8) (ret int) + +// CountBitsUint16sPopCnt count 1's in x +func CountBitsUint16sPopCnt(x []uint16) (ret int) + +// CountBitsUint32sPopCnt count 1's in x +func CountBitsUint32sPopCnt(x []uint32) (ret int) + +// CountBitsUint64sPopCnt count 1's in x +func CountBitsUint64sPopCnt(x []uint64) (ret int) + +// CountBitsUintsPopCnt count 1's in x +func CountBitsUintsPopCnt(x []uint) int { + if strconv.IntSize == 64 { + y := (*[]uint64)(unsafe.Pointer(&x)) // #nosec G103 + return CountBitsUint64sPopCnt(*y) + } else if strconv.IntSize == 32 { + y := (*[]uint32)(unsafe.Pointer(&x)) // #nosec G103 + return CountBitsUint32sPopCnt(*y) + } + panic("strconv.IntSize must be 32 or 64 bits") +} + +// CountBitsBytesPopCnt count 1's in x +func CountBitsBytesPopCnt(x []byte) (ret int) + +// CountBitsRunesPopCnt count 1's in x +func CountBitsRunesPopCnt(x []rune) (ret int) + +// CountBitsStringPopCnt count 1's in s +func CountBitsStringPopCnt(s string) (ret int) diff --git a/vendor/github.com/steakknife/hamming/popcount_slices_amd64.s b/vendor/github.com/steakknife/hamming/popcount_slices_amd64.s new file mode 100644 index 0000000000..b6b8c7835a --- /dev/null +++ b/vendor/github.com/steakknife/hamming/popcount_slices_amd64.s @@ -0,0 +1,370 @@ +// +// hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016 Barry Allard +// +// MIT license +// + +#include "textflag.h" + +// type SliceHeader struct { +// Data uintptr 0 +// Len int 8 +// Cap int 16 +// } + +// 0 x.Data +// 8 x.Len +// 16 x.Cap +// 24 ret + +// type StringHeader struct { +// Data uintptr 0 +// Len int 8 +// } + +// 0 x.Data +// 8 x.Len +// 16 ret + +// func CountBitsInt8sPopCnt(x []int8) (ret int) +TEXT ·CountBitsInt8sPopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint8sPopCnt(SB) + +// func CountBitsInt16sPopCnt(x []int16) (ret int) +TEXT ·CountBitsInt16sPopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint16sPopCnt(SB) + +// func CountBitsInt32sPopCnt(x []int32) (ret int) +TEXT ·CountBitsInt32sPopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint32sPopCnt(SB) + +// func CountBitsInt64sPopCnt(x []int64) (ret int) +TEXT ·CountBitsInt64sPopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint64sPopCnt(SB) + +// func CountBitsUint8sPopCnt(x []uint8) (ret int) +TEXT ·CountBitsUint8sPopCnt(SB),NOSPLIT,$0 + XORQ AX, AX // ret = 0 + MOVQ x+8(FP), CX // x.Len -> CX + +test_negative_slice_len: + MOVQ CX, BX // x.Len < 0 ---> x.Len[63] != 0 + SHRQ $63, BX + JNZ done + + MOVQ x+0(FP), DI // x.Data -> DI + + CMPQ CX, $32 // x.Len >= 32 + JL unrolled_loop_skip + +unrolled_loop_setup: + XORQ R9, R9 + XORQ BX, BX + XORQ DX, DX + +unrolled_loop: // 4 unrolled loops of POPCNTQ (4 quad words at a time) + SUBQ $32, CX + + POPCNTQ 0(DI), R10 + ADDQ R10, R9 + POPCNTQ 8(DI), R11 + ADDQ R11, AX + POPCNTQ 16(DI), R12 + ADDQ R12, BX + POPCNTQ 24(DI), R13 + ADDQ R13, DX + + ADDQ $32, DI + CMPQ CX, $32 // x.Len >= 32 + JGE unrolled_loop + +unrolled_loop_done: + ADDQ R9, AX + ADDQ BX, DX + ADDQ DX, AX + + XORQ BX, BX + +unrolled_loop_skip: + CMPQ CX, $0 + JZ done + + XORQ DX, DX + +remainder_loop: + MOVB 0(DI), DL + POPCNTQ DX, BX + ADDQ BX, AX + + INCQ DI + DECQ CX + JNZ remainder_loop + +done: + MOVQ AX, ret+24(FP) + RET + +// func CountBitsUint16sPopCnt(x []uint16) (ret int) +TEXT ·CountBitsUint16sPopCnt(SB),NOSPLIT,$0 + XORQ AX, AX // ret = 0 + MOVQ x+8(FP), CX // x.Len -> CX + +test_negative_slice_len: + MOVQ CX, BX // x.Len*2 < 0 ---> x.Len[63:62] != 0 + SHLQ $1, CX + SHRQ $62, BX + JNZ done + + MOVQ x+0(FP), DI // x.Data -> DI + + + CMPQ CX, $32 // x.Len*2 >= 32 + JL unrolled_loop_skip + +unrolled_loop_setup: + XORQ R9, R9 + XORQ BX, BX + XORQ DX, DX + +unrolled_loop: // 4 unrolled loops of POPCNTQ (4 quad words at a time) + SUBQ $32, CX + + POPCNTQ 0(DI), R10 + ADDQ R10, R9 + POPCNTQ 8(DI), R11 + ADDQ R11, AX + POPCNTQ 16(DI), R12 + ADDQ R12, BX + POPCNTQ 24(DI), R13 + ADDQ R13, DX + + ADDQ $32, DI + CMPQ CX, $32 // x.Len*2 >= 32 + JGE unrolled_loop + +unrolled_loop_done: + ADDQ R9, AX + ADDQ BX, DX + ADDQ DX, AX + + XORQ BX, BX + +unrolled_loop_skip: + CMPQ CX, $0 + JZ done + + XORQ DX, DX + +remainder_loop: + MOVW 0(DI), DX + POPCNTQ DX, BX + ADDQ BX, AX + + ADDQ $2, DI + SUBQ $2, CX + JNZ remainder_loop + +done: + MOVQ AX, ret+24(FP) + RET + +// func CountBitsUint32sPopCnt(x []uint32) (ret int) +TEXT ·CountBitsUint32sPopCnt(SB),NOSPLIT,$0 + XORQ AX, AX // ret = 0 + MOVQ x+8(FP), CX // x.Len -> CX + MOVQ CX, BX + MOVQ x+0(FP), DI // x.Data -> DI + +test_negative_slice_len: + SHLQ $2, CX // x.Len*4 < 0 ---> x.Len[63:61] != 0 + SHRQ $61, BX + JNZ done + + + + CMPQ CX, $32 // x.Len*4 >= 32 + JL unrolled_loop_skip + +unrolled_loop_setup: + XORQ R9, R9 + XORQ BX, BX + XORQ DX, DX + +unrolled_loop: // 4 unrolled loops of POPCNTQ (4 quad words at a time) + SUBQ $32, CX + + POPCNTQ 0(DI), R10 // r9 += popcntq(QW DI+0) + ADDQ R10, R9 + POPCNTQ 8(DI), R11 // ax += popcntq(QW DI+8) + ADDQ R11, AX + POPCNTQ 16(DI), R12 // bx += popcntq(QW DI+16) + ADDQ R12, BX + POPCNTQ 24(DI), R13 // dx += popcntq(QW DI+24) + ADDQ R13, DX + + ADDQ $32, DI + CMPQ CX, $32 // x.Len*4 >= 32 + JGE unrolled_loop + +unrolled_loop_done: + ADDQ R9, AX // ax = (ax + r9) + (bx + dx) + ADDQ BX, DX + ADDQ DX, AX + + XORQ BX, BX + +unrolled_loop_skip: + CMPQ CX, $0 + JZ done + + XORQ DX, DX +remainder_loop: + MOVB (DI), DX // ax += popcnt(DB 0(DI)) + POPCNTQ DX, BX + ADDQ BX, AX + + INCQ DI + DECQ CX + JNZ remainder_loop + +done: + MOVQ AX, ret+24(FP) + RET + +// func CountBitsUint64sPopCnt(x []uint64) (ret int) +TEXT ·CountBitsUint64sPopCnt(SB),NOSPLIT,$0 + XORQ AX, AX // ret = 0 + MOVQ x+8(FP), CX // x.Len -> CX + +test_negative_slice_len: + MOVQ CX, BX // x.Len*8 < 0 ---> x.Len[63:60] != 0 + SHLQ $3, CX + SHRQ $60, BX + JNZ done + + MOVQ x+0(FP), DI // x.Data -> DI + + + CMPQ CX, $32 // x.Len*8 >= 32 + JL unrolled_loop_skip + +unrolled_loop_setup: + XORQ R9, R9 + XORQ BX, BX + XORQ DX, DX + +unrolled_loop: // 4 unrolled loops of POPCNTQ (4 quad words at a time) + SUBQ $32, CX + + POPCNTQ 0(DI), R10 + ADDQ R10, R9 + POPCNTQ 8(DI), R11 + ADDQ R11, AX + POPCNTQ 16(DI), R12 + ADDQ R12, BX + POPCNTQ 24(DI), R13 + ADDQ R13, DX + + ADDQ $32, DI + CMPQ CX, $32 // x.Len*4 >= 32 + JGE unrolled_loop + +unrolled_loop_done: + ADDQ R9, AX + ADDQ BX, DX + ADDQ DX, AX + + XORQ BX, BX + +unrolled_loop_skip: + CMPQ CX, $0 + JZ done + + XORQ DX, DX + +remainder_loop: + MOVQ 0(DI), DX + POPCNTQ DX, BX + ADDQ BX, AX + + ADDQ $8, DI + SUBQ $8, CX + JNZ remainder_loop + +done: + MOVQ AX, ret+24(FP) + RET + +// func CountBitsBytesPopCnt(x []byte) (ret int) +TEXT ·CountBitsBytesPopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint8sPopCnt(SB) + +// func CountBitsRunesPopCnt(x []rune) (ret int) +TEXT ·CountBitsRunesPopCnt(SB),NOSPLIT,$0 + JMP ·CountBitsUint32sPopCnt(SB) + +// func CountBitsStringPopCnt(s string) (ret int) +TEXT ·CountBitsStringPopCnt(SB),NOSPLIT,$0 + XORQ AX, AX // ret = 0 + MOVQ x+8(FP), CX // x.Len -> CX + +test_negative_slice_len: + MOVQ CX, BX // x.Len < 0 ---> x.Len[63] != 0 + SHRQ $63, BX + JNZ done + + MOVQ x+0(FP), DI // x.Data -> DI + + CMPQ CX, $32 // x.Len >= 32 + JL unrolled_loop_skip + +unrolled_loop_setup: + XORQ R9, R9 + XORQ BX, BX + XORQ DX, DX + +unrolled_loop: // 4 unrolled loops of POPCNTQ (4 quad words at a time) + SUBQ $32, CX + + POPCNTQ 0(DI), R10 + ADDQ R10, R9 + POPCNTQ 8(DI), R11 + ADDQ R11, AX + POPCNTQ 16(DI), R12 + ADDQ R12, BX + POPCNTQ 24(DI), R13 + ADDQ R13, DX + + ADDQ $32, DI + CMPQ CX, $32 // x.Len >= 32 + JGE unrolled_loop + +unrolled_loop_done: + ADDQ R9, AX + ADDQ BX, DX + ADDQ DX, AX + + XORQ BX, BX + +unrolled_loop_skip: + CMPQ CX, $0 + JZ done + + XORQ DX, DX + +remainder_loop: + MOVB 0(DI), DL + POPCNTQ DX, BX + ADDQ BX, AX + + INCQ DI + DECQ CX + JNZ remainder_loop + +done: + MOVQ AX, ret+16(FP) + RET diff --git a/vendor/github.com/steakknife/hamming/slices_of_hamming.go b/vendor/github.com/steakknife/hamming/slices_of_hamming.go new file mode 100644 index 0000000000..82ce948fac --- /dev/null +++ b/vendor/github.com/steakknife/hamming/slices_of_hamming.go @@ -0,0 +1,144 @@ +// +// Package hamming distance calculations in Go +// +// https://github.com/steakknife/hamming +// +// Copyright © 2014, 2015, 2016, 2018 Barry Allard +// +// MIT license +// +package hamming + +// Int8s hamming distance of two int8 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Int8s(b0, b1 []int8) int { + d := 0 + for i, x := range b0 { + d += Int8(x, b1[i]) + } + return d +} + +// Int16s hamming distance of two int16 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Int16s(b0, b1 []int16) int { + d := 0 + for i, x := range b0 { + d += Int16(x, b1[i]) + } + return d +} + +// Int32s hamming distance of two int32 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Int32s(b0, b1 []int32) int { + d := 0 + for i, x := range b0 { + d += Int32(x, b1[i]) + } + return d +} + +// Int64s hamming distance of two int64 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Int64s(b0, b1 []int64) int { + d := 0 + for i, x := range b0 { + d += Int64(x, b1[i]) + } + return d +} + +// Ints hamming distance of two int buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Ints(b0, b1 []int) int { + d := 0 + for i, x := range b0 { + d += Int(x, b1[i]) + } + return d +} + +// Uint8s hamming distance of two uint8 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Uint8s(b0, b1 []uint8) int { + d := 0 + for i, x := range b0 { + d += Uint8(x, b1[i]) + } + return d +} + +// Uint16s hamming distance of two uint16 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Uint16s(b0, b1 []uint16) int { + d := 0 + for i, x := range b0 { + d += Uint16(x, b1[i]) + } + return d +} + +// Uint32s hamming distance of two uint32 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Uint32s(b0, b1 []uint32) int { + d := 0 + for i, x := range b0 { + d += Uint32(x, b1[i]) + } + return d +} + +// Uint64s hamming distance of two uint64 buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Uint64s(b0, b1 []uint64) int { + d := 0 + for i, x := range b0 { + d += Uint64(x, b1[i]) + } + return d +} + +// Uints hamming distance of two uint buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Uints(b0, b1 []uint) int { + d := 0 + for i, x := range b0 { + d += Uint(x, b1[i]) + } + return d +} + +// Bytes hamming distance of two byte buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Bytes(b0, b1 []byte) int { + d := 0 + for i, x := range b0 { + d += Byte(x, b1[i]) + } + return d +} + +// Runes hamming distance of two rune buffers, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Runes(b0, b1 []rune) int { + d := 0 + for i, x := range b0 { + d += Rune(x, b1[i]) + } + return d +} + +// Strings hamming distance of two strings, of which the size of b0 +// is used for both (panics if b1 < b0, does not compare b1 beyond length of b0) +func Strings(b0, b1 string) int { + return Runes(runes(b0), runes(b1)) +} + +// runize string +func runes(s string) (r []rune) { + for _, ch := range s { + r = append(r, ch) + } + return +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 89a78363c7..c760d2cb77 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -436,6 +436,18 @@ "revision": "8537d3370df43a30a3d450c023783d2e43432b89", "revisionTime": "2019-03-16T09:03:35Z" }, + { + "checksumSHA1": "1mI7DMaBgFAsU0aCrW5yCKyAPdM=", + "path": "github.com/steakknife/bloomfilter", + "revision": "6819c0d2a57025e1700378ee59b7382d71987f14", + "revisionTime": "2018-09-22T17:46:46Z" + }, + { + "checksumSHA1": "uuF97bplG/+iQ/nfNSQGZOmTKBE=", + "path": "github.com/steakknife/hamming", + "revision": "c99c65617cd3d686aea8365fe563d6542f01d940", + "revisionTime": "2018-09-06T05:59:17Z" + }, { "checksumSHA1": "mGbTYZ8dHVTiPTTJu3ktp+84pPI=", "path": "github.com/stretchr/testify/assert", From 184af72e4e30266b1f25b7602cc1ffdc60a130f6 Mon Sep 17 00:00:00 2001 From: Jeremy Schlatter Date: Tue, 14 May 2019 02:38:34 -0700 Subject: [PATCH 11/34] accounts/abi: fix documentation (#19568) --- accounts/abi/method.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/abi/method.go b/accounts/abi/method.go index 2d8d3d6589..d3c02599f2 100644 --- a/accounts/abi/method.go +++ b/accounts/abi/method.go @@ -27,9 +27,9 @@ import ( // If the method is `Const` no transaction needs to be created for this // particular Method call. It can easily be simulated using a local VM. // For example a `Balance()` method only needs to retrieve something -// from the storage and therefor requires no Tx to be send to the +// from the storage and therefore requires no Tx to be send to the // network. A method such as `Transact` does require a Tx and thus will -// be flagged `true`. +// be flagged `false`. // Input specifies the required input parameters for this gives method. type Method struct { Name string From 8deec2e45a315393025145ace43782b6294541d5 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 14 May 2019 15:09:56 +0200 Subject: [PATCH 12/34] rlp: fixes for two corner cases and documentation (#19527) These changes fix two corner cases related to internal handling of types in package rlp: The "tail" struct tag can only be applied to the last field. The check for this was wrong and didn't allow for private fields after the field with the tag. Unsupported types (e.g. structs containing int) which implement either the Encoder or Decoder interface but not both couldn't be encoded/decoded. Also fixes #19367 --- rlp/decode.go | 30 ++++++++++---------- rlp/decode_test.go | 32 +++++++++++++++++++++ rlp/encode.go | 28 ++++++++++--------- rlp/encode_test.go | 9 ++++++ rlp/typecache.go | 69 ++++++++++++++++++++++++++-------------------- 5 files changed, 111 insertions(+), 57 deletions(-) diff --git a/rlp/decode.go b/rlp/decode.go index e638c01eaf..4f29f2fb03 100644 --- a/rlp/decode.go +++ b/rlp/decode.go @@ -115,15 +115,17 @@ type Decoder interface { // type, Decode will return an error. Decode also supports *big.Int. // There is no size limit for big integers. // +// To decode into a boolean, the input must contain an unsigned integer +// of value zero (false) or one (true). +// // To decode into an interface value, Decode stores one of these // in the value: // // []interface{}, for RLP lists // []byte, for RLP strings // -// Non-empty interface types are not supported, nor are booleans, -// signed integers, floating point numbers, maps, channels and -// functions. +// Non-empty interface types are not supported, nor are signed integers, +// floating point numbers, maps, channels and functions. // // Note that Decode does not set an input limit for all readers // and may be vulnerable to panics cause by huge value sizes. If @@ -306,9 +308,9 @@ func makeListDecoder(typ reflect.Type, tag tags) (decoder, error) { } return decodeByteSlice, nil } - etypeinfo, err := cachedTypeInfo1(etype, tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(etype, tags{}) + if etypeinfo.decoderErr != nil { + return nil, etypeinfo.decoderErr } var dec decoder switch { @@ -467,9 +469,9 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) { // the pointer's element type. func makePtrDecoder(typ reflect.Type) (decoder, error) { etype := typ.Elem() - etypeinfo, err := cachedTypeInfo1(etype, tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(etype, tags{}) + if etypeinfo.decoderErr != nil { + return nil, etypeinfo.decoderErr } dec := func(s *Stream, val reflect.Value) (err error) { newval := val @@ -491,9 +493,9 @@ func makePtrDecoder(typ reflect.Type) (decoder, error) { // This decoder is used for pointer-typed struct fields with struct tag "nil". func makeOptionalPtrDecoder(typ reflect.Type) (decoder, error) { etype := typ.Elem() - etypeinfo, err := cachedTypeInfo1(etype, tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(etype, tags{}) + if etypeinfo.decoderErr != nil { + return nil, etypeinfo.decoderErr } dec := func(s *Stream, val reflect.Value) (err error) { kind, size, err := s.Kind() @@ -814,12 +816,12 @@ func (s *Stream) Decode(val interface{}) error { if rval.IsNil() { return errDecodeIntoNil } - info, err := cachedTypeInfo(rtyp.Elem(), tags{}) + decoder, err := cachedDecoder(rtyp.Elem()) if err != nil { return err } - err = info.decoder(s, rval.Elem()) + err = decoder(s, rval.Elem()) if decErr, ok := err.(*decodeError); ok && len(decErr.ctx) > 0 { // add decode target type to error so context has more meaning decErr.ctx = append(decErr.ctx, fmt.Sprint("(", rtyp.Elem(), ")")) diff --git a/rlp/decode_test.go b/rlp/decode_test.go index 4d8abd0012..fa57182c99 100644 --- a/rlp/decode_test.go +++ b/rlp/decode_test.go @@ -347,6 +347,12 @@ type tailUint struct { Tail []uint `rlp:"tail"` } +type tailPrivateFields struct { + A uint + Tail []uint `rlp:"tail"` + x, y bool +} + var ( veryBigInt = big.NewInt(0).Add( big.NewInt(0).Lsh(big.NewInt(0xFFFFFFFFFFFFFF), 16), @@ -510,6 +516,11 @@ var decodeTests = []decodeTest{ ptr: new(tailRaw), value: tailRaw{A: 1, Tail: []RawValue{}}, }, + { + input: "C3010203", + ptr: new(tailPrivateFields), + value: tailPrivateFields{A: 1, Tail: []uint{2, 3}}, + }, // struct tag "-" { @@ -691,6 +702,27 @@ func TestDecoderInByteSlice(t *testing.T) { } } +type unencodableDecoder func() + +func (f *unencodableDecoder) DecodeRLP(s *Stream) error { + if _, err := s.List(); err != nil { + return err + } + if err := s.ListEnd(); err != nil { + return err + } + *f = func() {} + return nil +} + +func TestDecoderFunc(t *testing.T) { + var x func() + if err := DecodeBytes([]byte{0xC0}, (*unencodableDecoder)(&x)); err != nil { + t.Fatal(err) + } + x() +} + func ExampleDecode() { input, _ := hex.DecodeString("C90A1486666F6F626172") diff --git a/rlp/encode.go b/rlp/encode.go index 445b4b5b21..f255c38a9c 100644 --- a/rlp/encode.go +++ b/rlp/encode.go @@ -73,10 +73,12 @@ type Encoder interface { // An unsigned integer value is encoded as an RLP string. Zero always // encodes as an empty RLP string. Encode also supports *big.Int. // +// Boolean values are encoded as unsigned integers zero (false) and one (true). +// // An interface value encodes as the value contained in the interface. // -// Boolean values are not supported, nor are signed integers, floating -// point numbers, maps, channels and functions. +// Signed integers are not supported, nor are floating point numbers, maps, +// channels and functions. func Encode(w io.Writer, val interface{}) error { if outer, ok := w.(*encbuf); ok { // Encode was called by some type's EncodeRLP. @@ -180,11 +182,11 @@ func (w *encbuf) Write(b []byte) (int, error) { func (w *encbuf) encode(val interface{}) error { rval := reflect.ValueOf(val) - ti, err := cachedTypeInfo(rval.Type(), tags{}) + writer, err := cachedWriter(rval.Type()) if err != nil { return err } - return ti.writer(rval, w) + return writer(rval, w) } func (w *encbuf) encodeStringHeader(size int) { @@ -497,17 +499,17 @@ func writeInterface(val reflect.Value, w *encbuf) error { return nil } eval := val.Elem() - ti, err := cachedTypeInfo(eval.Type(), tags{}) + writer, err := cachedWriter(eval.Type()) if err != nil { return err } - return ti.writer(eval, w) + return writer(eval, w) } func makeSliceWriter(typ reflect.Type, ts tags) (writer, error) { - etypeinfo, err := cachedTypeInfo1(typ.Elem(), tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(typ.Elem(), tags{}) + if etypeinfo.writerErr != nil { + return nil, etypeinfo.writerErr } writer := func(val reflect.Value, w *encbuf) error { if !ts.tail { @@ -543,9 +545,9 @@ func makeStructWriter(typ reflect.Type) (writer, error) { } func makePtrWriter(typ reflect.Type) (writer, error) { - etypeinfo, err := cachedTypeInfo1(typ.Elem(), tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(typ.Elem(), tags{}) + if etypeinfo.writerErr != nil { + return nil, etypeinfo.writerErr } // determine nil pointer handler @@ -577,7 +579,7 @@ func makePtrWriter(typ reflect.Type) (writer, error) { } return etypeinfo.writer(val.Elem(), w) } - return writer, err + return writer, nil } // putint writes i to the beginning of b in big endian byte diff --git a/rlp/encode_test.go b/rlp/encode_test.go index 827960f7c1..6e49b89a85 100644 --- a/rlp/encode_test.go +++ b/rlp/encode_test.go @@ -49,6 +49,13 @@ func (e byteEncoder) EncodeRLP(w io.Writer) error { return nil } +type undecodableEncoder func() + +func (f undecodableEncoder) EncodeRLP(w io.Writer) error { + _, err := w.Write(EmptyList) + return err +} + type encodableReader struct { A, B uint } @@ -239,6 +246,8 @@ var encTests = []encTest{ {val: (*testEncoder)(nil), output: "00000000"}, {val: &testEncoder{}, output: "00010001000100010001"}, {val: &testEncoder{errors.New("test error")}, error: "test error"}, + // verify that the Encoder interface works for unsupported types like func(). + {val: undecodableEncoder(func() {}), output: "C0"}, // verify that pointer method testEncoder.EncodeRLP is called for // addressable non-pointer values. {val: &struct{ TE testEncoder }{testEncoder{}}, output: "CA00010001000100010001"}, diff --git a/rlp/typecache.go b/rlp/typecache.go index 8c2dd518e2..ab5ee3da76 100644 --- a/rlp/typecache.go +++ b/rlp/typecache.go @@ -29,8 +29,10 @@ var ( ) type typeinfo struct { - decoder - writer + decoder decoder + decoderErr error // error from makeDecoder + writer writer + writerErr error // error from makeWriter } // represents struct tags @@ -56,12 +58,22 @@ type decoder func(*Stream, reflect.Value) error type writer func(reflect.Value, *encbuf) error -func cachedTypeInfo(typ reflect.Type, tags tags) (*typeinfo, error) { +func cachedDecoder(typ reflect.Type) (decoder, error) { + info := cachedTypeInfo(typ, tags{}) + return info.decoder, info.decoderErr +} + +func cachedWriter(typ reflect.Type) (writer, error) { + info := cachedTypeInfo(typ, tags{}) + return info.writer, info.writerErr +} + +func cachedTypeInfo(typ reflect.Type, tags tags) *typeinfo { typeCacheMutex.RLock() info := typeCache[typekey{typ, tags}] typeCacheMutex.RUnlock() if info != nil { - return info, nil + return info } // not in the cache, need to generate info for this type. typeCacheMutex.Lock() @@ -69,25 +81,20 @@ func cachedTypeInfo(typ reflect.Type, tags tags) (*typeinfo, error) { return cachedTypeInfo1(typ, tags) } -func cachedTypeInfo1(typ reflect.Type, tags tags) (*typeinfo, error) { +func cachedTypeInfo1(typ reflect.Type, tags tags) *typeinfo { key := typekey{typ, tags} info := typeCache[key] if info != nil { // another goroutine got the write lock first - return info, nil + return info } // put a dummy value into the cache before generating. // if the generator tries to lookup itself, it will get // the dummy value and won't call itself recursively. - typeCache[key] = new(typeinfo) - info, err := genTypeInfo(typ, tags) - if err != nil { - // remove the dummy value if the generator fails - delete(typeCache, key) - return nil, err - } - *typeCache[key] = *info - return typeCache[key], err + info = new(typeinfo) + typeCache[key] = info + info.generate(typ, tags) + return info } type field struct { @@ -96,26 +103,24 @@ type field struct { } func structFields(typ reflect.Type) (fields []field, err error) { + lastPublic := lastPublicField(typ) for i := 0; i < typ.NumField(); i++ { if f := typ.Field(i); f.PkgPath == "" { // exported - tags, err := parseStructTag(typ, i) + tags, err := parseStructTag(typ, i, lastPublic) if err != nil { return nil, err } if tags.ignored { continue } - info, err := cachedTypeInfo1(f.Type, tags) - if err != nil { - return nil, err - } + info := cachedTypeInfo1(f.Type, tags) fields = append(fields, field{i, info}) } } return fields, nil } -func parseStructTag(typ reflect.Type, fi int) (tags, error) { +func parseStructTag(typ reflect.Type, fi, lastPublic int) (tags, error) { f := typ.Field(fi) var ts tags for _, t := range strings.Split(f.Tag.Get("rlp"), ",") { @@ -127,7 +132,7 @@ func parseStructTag(typ reflect.Type, fi int) (tags, error) { ts.nilOK = true case "tail": ts.tail = true - if fi != typ.NumField()-1 { + if fi != lastPublic { return ts, fmt.Errorf(`rlp: invalid struct tag "tail" for %v.%s (must be on last field)`, typ, f.Name) } if f.Type.Kind() != reflect.Slice { @@ -140,15 +145,19 @@ func parseStructTag(typ reflect.Type, fi int) (tags, error) { return ts, nil } -func genTypeInfo(typ reflect.Type, tags tags) (info *typeinfo, err error) { - info = new(typeinfo) - if info.decoder, err = makeDecoder(typ, tags); err != nil { - return nil, err +func lastPublicField(typ reflect.Type) int { + last := 0 + for i := 0; i < typ.NumField(); i++ { + if typ.Field(i).PkgPath == "" { + last = i + } } - if info.writer, err = makeWriter(typ, tags); err != nil { - return nil, err - } - return info, nil + return last +} + +func (i *typeinfo) generate(typ reflect.Type, tags tags) { + i.decoder, i.decoderErr = makeDecoder(typ, tags) + i.writer, i.writerErr = makeWriter(typ, tags) } func isUint(k reflect.Kind) bool { From 350a87dd3c8250db50326fff076df3fd21d8f69f Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 15 May 2019 06:47:45 +0200 Subject: [PATCH 13/34] p2p/discover: add support for EIP-868 (v4 ENR extension) (#19540) This change implements EIP-868. The UDPv4 transport announces support for the extension in ping/pong and handles enrRequest messages. There are two uses of the extension: If a remote node announces support for EIP-868 in their pong, node revalidation pulls the node's record. The Resolve method requests the record unconditionally. --- p2p/discover/table.go | 60 +++--- p2p/discover/table_test.go | 28 +++ p2p/discover/table_util_test.go | 48 +++-- p2p/discover/v4_udp.go | 302 ++++++++++++++++++++++------- p2p/discover/v4_udp_lookup_test.go | 4 +- p2p/discover/v4_udp_test.go | 98 +++++++--- 6 files changed, 405 insertions(+), 135 deletions(-) diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 61c62f1878..3460e8377c 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -53,15 +53,17 @@ const ( bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24 tableIPLimit, tableSubnet = 10, 24 - maxFindnodeFailures = 5 // Nodes exceeding this limit are dropped - refreshInterval = 30 * time.Minute - revalidateInterval = 10 * time.Second - copyNodesInterval = 30 * time.Second - seedMinTableTime = 5 * time.Minute - seedCount = 30 - seedMaxAge = 5 * 24 * time.Hour + refreshInterval = 30 * time.Minute + revalidateInterval = 10 * time.Second + copyNodesInterval = 30 * time.Second + seedMinTableTime = 5 * time.Minute + seedCount = 30 + seedMaxAge = 5 * 24 * time.Hour ) +// Table is the 'node table', a Kademlia-like index of neighbor nodes. The table keeps +// itself up-to-date by verifying the liveness of neighbors and requesting their node +// records when announcements of a new record version are received. type Table struct { mutex sync.Mutex // protects buckets, bucket content, nursery, rand buckets [nBuckets]*bucket // index of known nodes by distance @@ -80,12 +82,13 @@ type Table struct { nodeAddedHook func(*node) // for testing } -// transport is implemented by UDP transports. +// transport is implemented by the UDP transports. type transport interface { Self() *enode.Node lookupRandom() []*enode.Node lookupSelf() []*enode.Node - ping(*enode.Node) error + ping(*enode.Node) (seq uint64, err error) + requestENR(*enode.Node) (*enode.Node, error) } // bucket contains nodes, ordered by their last activity. the entry @@ -175,14 +178,16 @@ func (tab *Table) ReadRandomNodes(buf []*enode.Node) (n int) { return i + 1 } -// Resolve searches for a specific node with the given ID. -// It returns nil if the node could not be found. -func (tab *Table) Resolve(n *enode.Node) *enode.Node { +// getNode returns the node with the given ID or nil if it isn't in the table. +func (tab *Table) getNode(id enode.ID) *enode.Node { tab.mutex.Lock() - cl := tab.closest(n.ID(), 1, false) - tab.mutex.Unlock() - if len(cl.entries) > 0 && cl.entries[0].ID() == n.ID() { - return unwrapNode(cl.entries[0]) + defer tab.mutex.Unlock() + + b := tab.bucket(id) + for _, e := range b.entries { + if e.ID() == id { + return unwrapNode(e) + } } return nil } @@ -226,7 +231,7 @@ func (tab *Table) refresh() <-chan struct{} { return done } -// loop schedules refresh, revalidate runs and coordinates shutdown. +// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes. func (tab *Table) loop() { var ( revalidate = time.NewTimer(tab.nextRevalidateTime()) @@ -288,9 +293,8 @@ loop: close(tab.closed) } -// doRefresh performs a lookup for a random target to keep buckets -// full. seed nodes are inserted if the table is empty (initial -// bootstrap or discarded faulty peers). +// doRefresh performs a lookup for a random target to keep buckets full. seed nodes are +// inserted if the table is empty (initial bootstrap or discarded faulty peers). func (tab *Table) doRefresh(done chan struct{}) { defer close(done) @@ -324,8 +328,8 @@ func (tab *Table) loadSeedNodes() { } } -// doRevalidate checks that the last node in a random bucket is still live -// and replaces or deletes the node if it isn't. +// doRevalidate checks that the last node in a random bucket is still live and replaces or +// deletes the node if it isn't. func (tab *Table) doRevalidate(done chan<- struct{}) { defer func() { done <- struct{}{} }() @@ -336,7 +340,17 @@ func (tab *Table) doRevalidate(done chan<- struct{}) { } // Ping the selected node and wait for a pong. - err := tab.net.ping(unwrapNode(last)) + remoteSeq, err := tab.net.ping(unwrapNode(last)) + + // Also fetch record if the node replied and returned a higher sequence number. + if last.Seq() < remoteSeq { + n, err := tab.net.requestENR(unwrapNode(last)) + if err != nil { + tab.log.Debug("ENR request failed", "id", last.ID(), "addr", last.addr(), "err", err) + } else { + last = &node{Node: *n, addedAt: last.addedAt, livenessChecks: last.livenessChecks} + } + } tab.mutex.Lock() defer tab.mutex.Unlock() diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index 81763e7fea..895c284b27 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -368,6 +368,34 @@ func TestTable_addSeenNode(t *testing.T) { checkIPLimitInvariant(t, tab) } +// This test checks that ENR updates happen during revalidation. If a node in the table +// announces a new sequence number, the new record should be pulled. +func TestTable_revalidateSyncRecord(t *testing.T) { + transport := newPingRecorder() + tab, db := newTestTable(transport) + <-tab.initDone + defer db.Close() + defer tab.close() + + // Insert a node. + var r enr.Record + r.Set(enr.IP(net.IP{127, 0, 0, 1})) + id := enode.ID{1} + n1 := wrapNode(enode.SignNull(&r, id)) + tab.addSeenNode(n1) + + // Update the node record. + r.Set(enr.WithEntry("foo", "bar")) + n2 := enode.SignNull(&r, id) + transport.updateRecord(n2) + + tab.doRevalidate(make(chan struct{}, 1)) + intable := tab.getNode(id) + if !reflect.DeepEqual(intable, n2) { + t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq()) + } +} + // gen wraps quick.Value so it's easier to use. // it generates a random value of the given value's type. func gen(typ interface{}, rand *rand.Rand) interface{} { diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go index 71cb1895b0..3075c43408 100644 --- a/p2p/discover/table_util_test.go +++ b/p2p/discover/table_util_test.go @@ -98,6 +98,7 @@ func fillTable(tab *Table, nodes []*node) { type pingRecorder struct { mu sync.Mutex dead, pinged map[enode.ID]bool + records map[enode.ID]*enode.Node n *enode.Node } @@ -107,38 +108,53 @@ func newPingRecorder() *pingRecorder { n := enode.SignNull(&r, enode.ID{}) return &pingRecorder{ - dead: make(map[enode.ID]bool), - pinged: make(map[enode.ID]bool), - n: n, + dead: make(map[enode.ID]bool), + pinged: make(map[enode.ID]bool), + records: make(map[enode.ID]*enode.Node), + n: n, } } -func (t *pingRecorder) Self() *enode.Node { - return nullNode +// setRecord updates a node record. Future calls to ping and +// requestENR will return this record. +func (t *pingRecorder) updateRecord(n *enode.Node) { + t.mu.Lock() + defer t.mu.Unlock() + t.records[n.ID()] = n } -func (t *pingRecorder) ping(n *enode.Node) error { +// Stubs to satisfy the transport interface. +func (t *pingRecorder) Self() *enode.Node { return nullNode } +func (t *pingRecorder) lookupSelf() []*enode.Node { return nil } +func (t *pingRecorder) lookupRandom() []*enode.Node { return nil } +func (t *pingRecorder) close() {} + +// ping simulates a ping request. +func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) { t.mu.Lock() defer t.mu.Unlock() t.pinged[n.ID()] = true if t.dead[n.ID()] { - return errTimeout - } else { - return nil + return 0, errTimeout } + if t.records[n.ID()] != nil { + seq = t.records[n.ID()].Seq() + } + return seq, nil } -func (t *pingRecorder) lookupSelf() []*enode.Node { - return nil -} +// requestENR simulates an ENR request. +func (t *pingRecorder) requestENR(n *enode.Node) (*enode.Node, error) { + t.mu.Lock() + defer t.mu.Unlock() -func (t *pingRecorder) lookupRandom() []*enode.Node { - return nil + if t.dead[n.ID()] || t.records[n.ID()] == nil { + return nil, errTimeout + } + return t.records[n.ID()], nil } -func (t *pingRecorder) close() {} - func hasDuplicates(slice []*node) bool { seen := make(map[enode.ID]bool) for i, e := range slice { diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index 3c68beac1c..b0b0053a7a 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/rlp" ) @@ -47,12 +48,12 @@ var ( errClosed = errors.New("socket closed") ) -// Timeouts const ( respTimeout = 500 * time.Millisecond expiration = 20 * time.Second bondExpiration = 24 * time.Hour + maxFindnodeFailures = 5 // nodes exceeding this limit are dropped ntpFailureThreshold = 32 // Continuous timeouts after which to check NTP ntpWarningCooldown = 10 * time.Minute // Minimum amount of time to pass before repeating NTP warning driftThreshold = 10 * time.Second // Allowed clock drift before warning user @@ -69,6 +70,8 @@ const ( p_pongV4 p_findnodeV4 p_neighborsV4 + p_enrRequestV4 + p_enrResponseV4 ) // RPC request structures @@ -112,6 +115,21 @@ type ( Rest []rlp.RawValue `rlp:"tail"` } + // enrRequestV4 queries for the remote node's record. + enrRequestV4 struct { + Expiration uint64 + // Ignore additional fields (for forward compatibility). + Rest []rlp.RawValue `rlp:"tail"` + } + + // enrResponseV4 is the reply to enrRequestV4. + enrResponseV4 struct { + ReplyTok []byte // Hash of the enrRequest packet. + Record enr.Record + // Ignore additional fields (for forward compatibility). + Rest []rlp.RawValue `rlp:"tail"` + } + rpcNode struct { IP net.IP // len 4 for IPv4 or 16 for IPv6 UDP uint16 // for discovery protocol @@ -126,14 +144,15 @@ type ( } ) -// packet is implemented by all v4 protocol messages. +// packetV4 is implemented by all v4 protocol messages. type packetV4 interface { // preverify checks whether the packet is valid and should be handled at all. preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error // handle handles the packet. handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []byte) - // name returns the name of the packet for logging purposes. + // packet name and type for logging purposes. name() string + kind() byte } func makeEndpoint(addr *net.UDPAddr, tcpPort uint16) rpcEndpoint { @@ -191,7 +210,7 @@ type UDPv4 struct { closing chan struct{} } -// pending represents a pending reply. +// replyMatcher represents a pending reply. // // Some implementations of the protocol wish to send more than one // reply packet to findnode. In general, any neighbors packet cannot @@ -217,17 +236,20 @@ type replyMatcher struct { // errc receives nil when the callback indicates completion or an // error if no further reply is received within the timeout. - errc chan<- error + errc chan error + + // reply contains the most recent reply. This field is safe for reading after errc has + // received a value. + reply packetV4 } type replyMatchFunc func(interface{}) (matched bool, requestDone bool) +// reply is a reply packet from a certain node. type reply struct { - from enode.ID - ip net.IP - ptype byte - data packetV4 - + from enode.ID + ip net.IP + data packetV4 // loop indicates whether there was // a matching request by sending on this channel. matched chan<- bool @@ -377,7 +399,8 @@ func (t *UDPv4) lookupWorker(n *node, targetKey encPubkey, reply chan<- []*node) t.tab.delete(n) } } else if fails > 0 { - t.db.UpdateFindFails(n.ID(), n.IP(), fails-1) + // Reset failure counter because it counts _consecutive_ failures. + t.db.UpdateFindFails(n.ID(), n.IP(), 0) } // Grab as many nodes as possible. Some of them might not be alive anymore, but we'll @@ -388,23 +411,34 @@ func (t *UDPv4) lookupWorker(n *node, targetKey encPubkey, reply chan<- []*node) reply <- r } -// Resolve searches for a specific node with the given ID. -// It returns nil if the node could not be found. +// Resolve searches for a specific node with the given ID and tries to get the most recent +// version of the node record for it. It returns n if the node could not be resolved. func (t *UDPv4) Resolve(n *enode.Node) *enode.Node { - // If the node is present in the local table, no - // network interaction is required. - if intab := t.tab.Resolve(n); intab != nil { - return intab + // Try asking directly. This works if the node is still responding on the endpoint we have. + if rn, err := t.requestENR(n); err == nil { + return rn } - // Otherwise, do a network lookup. - hash := n.ID() - result := t.LookupPubkey(n.Pubkey()) - for _, n := range result { - if n.ID() == hash { - return n + // Check table for the ID, we might have a newer version there. + if intable := t.tab.getNode(n.ID()); intable != nil && intable.Seq() > n.Seq() { + n = intable + if rn, err := t.requestENR(n); err == nil { + return rn } } - return nil + // Otherwise perform a network lookup. + var key *enode.Secp256k1 + if n.Load(key) != nil { + return n // no secp256k1 key + } + result := t.LookupPubkey((*ecdsa.PublicKey)(key)) + for _, rn := range result { + if rn.ID() == n.ID() { + if rn, err := t.requestENR(rn); err == nil { + return rn + } + } + } + return n } func (t *UDPv4) ourEndpoint() rpcEndpoint { @@ -414,28 +448,27 @@ func (t *UDPv4) ourEndpoint() rpcEndpoint { } // ping sends a ping message to the given node and waits for a reply. -func (t *UDPv4) ping(n *enode.Node) error { - return <-t.sendPing(n.ID(), &net.UDPAddr{IP: n.IP(), Port: n.UDP()}, nil) +func (t *UDPv4) ping(n *enode.Node) (seq uint64, err error) { + rm := t.sendPing(n.ID(), &net.UDPAddr{IP: n.IP(), Port: n.UDP()}, nil) + if err = <-rm.errc; err == nil { + seq = seqFromTail(rm.reply.(*pongV4).Rest) + } + return seq, err } // sendPing sends a ping message to the given node and invokes the callback // when the reply arrives. -func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) <-chan error { - req := &pingV4{ - Version: 4, - From: t.ourEndpoint(), - To: makeEndpoint(toaddr, 0), // TODO: maybe use known TCP port from DB - Expiration: uint64(time.Now().Add(expiration).Unix()), - } - packet, hash, err := t.encode(t.priv, p_pingV4, req) +func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) *replyMatcher { + req := t.makePing(toaddr) + packet, hash, err := t.encode(t.priv, req) if err != nil { errc := make(chan error, 1) errc <- err - return errc + return &replyMatcher{errc: errc} } // Add a matcher for the reply to the pending reply queue. Pongs are matched if they // reference the ping we're about to send. - errc := t.pending(toid, toaddr.IP, p_pongV4, func(p interface{}) (matched bool, requestDone bool) { + rm := t.pending(toid, toaddr.IP, p_pongV4, func(p interface{}) (matched bool, requestDone bool) { matched = bytes.Equal(p.(*pongV4).ReplyTok, hash) if matched && callback != nil { callback() @@ -445,25 +478,30 @@ func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) <- // Send the packet. t.localNode.UDPContact(toaddr) t.write(toaddr, toid, req.name(), packet) - return errc + return rm +} + +func (t *UDPv4) makePing(toaddr *net.UDPAddr) *pingV4 { + seq, _ := rlp.EncodeToBytes(t.localNode.Node().Seq()) + return &pingV4{ + Version: 4, + From: t.ourEndpoint(), + To: makeEndpoint(toaddr, 0), + Expiration: uint64(time.Now().Add(expiration).Unix()), + Rest: []rlp.RawValue{seq}, + } } // findnode sends a findnode request to the given node and waits until // the node has sent up to k neighbors. func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) { - // If we haven't seen a ping from the destination node for a while, it won't remember - // our endpoint proof and reject findnode. Solicit a ping first. - if time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration { - <-t.sendPing(toid, toaddr, nil) - // Wait for them to ping back and process our pong. - time.Sleep(respTimeout) - } + t.ensureBond(toid, toaddr) // Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is // active until enough nodes have been received. nodes := make([]*node, 0, bucketSize) nreceived := 0 - errc := t.pending(toid, toaddr.IP, p_neighborsV4, func(r interface{}) (matched bool, requestDone bool) { + rm := t.pending(toid, toaddr.IP, p_neighborsV4, func(r interface{}) (matched bool, requestDone bool) { reply := r.(*neighborsV4) for _, rn := range reply.Nodes { nreceived++ @@ -476,16 +514,56 @@ func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ( } return true, nreceived >= bucketSize }) - t.send(toaddr, toid, p_findnodeV4, &findnodeV4{ + t.send(toaddr, toid, &findnodeV4{ Target: target, Expiration: uint64(time.Now().Add(expiration).Unix()), }) - return nodes, <-errc + return nodes, <-rm.errc +} + +// requestENR sends enrRequest to the given node and waits for a response. +func (t *UDPv4) requestENR(n *enode.Node) (*enode.Node, error) { + addr := &net.UDPAddr{IP: n.IP(), Port: n.UDP()} + t.ensureBond(n.ID(), addr) + + req := &enrRequestV4{ + Expiration: uint64(time.Now().Add(expiration).Unix()), + } + packet, hash, err := t.encode(t.priv, req) + if err != nil { + return nil, err + } + // Add a matcher for the reply to the pending reply queue. Responses are matched if + // they reference the request we're about to send. + rm := t.pending(n.ID(), addr.IP, p_enrResponseV4, func(r interface{}) (matched bool, requestDone bool) { + matched = bytes.Equal(r.(*enrResponseV4).ReplyTok, hash) + return matched, matched + }) + // Send the packet and wait for the reply. + t.write(addr, n.ID(), req.name(), packet) + if err := <-rm.errc; err != nil { + return nil, err + } + // Verify the response record. + respN, err := enode.New(enode.ValidSchemes, &rm.reply.(*enrResponseV4).Record) + if err != nil { + return nil, err + } + if respN.ID() != n.ID() { + return nil, fmt.Errorf("invalid ID in response record") + } + if respN.Seq() < n.Seq() { + return n, nil // response record is older + } + if err := netutil.CheckRelayIP(addr.IP, respN.IP()); err != nil { + return nil, fmt.Errorf("invalid IP in response record: %v", err) + } + return respN, nil } // pending adds a reply matcher to the pending reply queue. // see the documentation of type replyMatcher for a detailed explanation. -func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) <-chan error { +func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) *replyMatcher { ch := make(chan error, 1) p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch} select { @@ -494,15 +572,15 @@ func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchF case <-t.closing: ch <- errClosed } - return ch + return p } // handleReply dispatches a reply packet, invoking reply matchers. It returns // whether any matcher considered the packet acceptable. -func (t *UDPv4) handleReply(from enode.ID, fromIP net.IP, ptype byte, req packetV4) bool { +func (t *UDPv4) handleReply(from enode.ID, fromIP net.IP, req packetV4) bool { matched := make(chan bool, 1) select { - case t.gotreply <- reply{from, fromIP, ptype, req, matched}: + case t.gotreply <- reply{from, fromIP, req, matched}: // loop will handle it return <-matched case <-t.closing: @@ -565,11 +643,12 @@ func (t *UDPv4) loop() { var matched bool // whether any replyMatcher considered the reply acceptable. for el := plist.Front(); el != nil; el = el.Next() { p := el.Value.(*replyMatcher) - if p.from == r.from && p.ptype == r.ptype && p.ip.Equal(r.ip) { + if p.from == r.from && p.ptype == r.data.kind() && p.ip.Equal(r.ip) { ok, requestDone := p.callback(r.data) matched = matched || ok // Remove the matcher if callback indicates that all replies have been received. if requestDone { + p.reply = r.data p.errc <- nil plist.Remove(el) } @@ -635,8 +714,8 @@ func init() { } } -func (t *UDPv4) send(toaddr *net.UDPAddr, toid enode.ID, ptype byte, req packetV4) ([]byte, error) { - packet, hash, err := t.encode(t.priv, ptype, req) +func (t *UDPv4) send(toaddr *net.UDPAddr, toid enode.ID, req packetV4) ([]byte, error) { + packet, hash, err := t.encode(t.priv, req) if err != nil { return hash, err } @@ -649,18 +728,19 @@ func (t *UDPv4) write(toaddr *net.UDPAddr, toid enode.ID, what string, packet [] return err } -func (t *UDPv4) encode(priv *ecdsa.PrivateKey, ptype byte, req interface{}) (packet, hash []byte, err error) { +func (t *UDPv4) encode(priv *ecdsa.PrivateKey, req packetV4) (packet, hash []byte, err error) { + name := req.name() b := new(bytes.Buffer) b.Write(headSpace) - b.WriteByte(ptype) + b.WriteByte(req.kind()) if err := rlp.Encode(b, req); err != nil { - t.log.Error("Can't encode discv4 packet", "err", err) + t.log.Error(fmt.Sprintf("Can't encode %s packet", name), "err", err) return nil, nil, err } packet = b.Bytes() sig, err := crypto.Sign(crypto.Keccak256(packet[headSize:]), priv) if err != nil { - t.log.Error("Can't sign discv4 packet", "err", err) + t.log.Error(fmt.Sprintf("Can't sign %s packet", name), "err", err) return nil, nil, err } copy(packet[macSize:], sig) @@ -743,6 +823,10 @@ func decodeV4(buf []byte) (packetV4, encPubkey, []byte, error) { req = new(findnodeV4) case p_neighborsV4: req = new(neighborsV4) + case p_enrRequestV4: + req = new(enrRequestV4) + case p_enrResponseV4: + req = new(enrResponseV4) default: return nil, fromKey, hash, fmt.Errorf("unknown type: %d", ptype) } @@ -751,7 +835,41 @@ func decodeV4(buf []byte) (packetV4, encPubkey, []byte, error) { return req, fromKey, hash, err } -// Packet Handlers +// checkBond checks if the given node has a recent enough endpoint proof. +func (t *UDPv4) checkBond(id enode.ID, ip net.IP) bool { + return time.Since(t.db.LastPongReceived(id, ip)) < bondExpiration +} + +// ensureBond solicits a ping from a node if we haven't seen a ping from it for a while. +// This ensures there is a valid endpoint proof on the remote end. +func (t *UDPv4) ensureBond(toid enode.ID, toaddr *net.UDPAddr) { + tooOld := time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration + if tooOld || t.db.FindFails(toid, toaddr.IP) > maxFindnodeFailures { + rm := t.sendPing(toid, toaddr, nil) + <-rm.errc + // Wait for them to ping back and process our pong. + time.Sleep(respTimeout) + } +} + +// expired checks whether the given UNIX time stamp is in the past. +func expired(ts uint64) bool { + return time.Unix(int64(ts), 0).Before(time.Now()) +} + +func seqFromTail(tail []rlp.RawValue) uint64 { + if len(tail) == 0 { + return 0 + } + var seq uint64 + rlp.DecodeBytes(tail[0], &seq) + return seq +} + +// PING/v4 + +func (req *pingV4) name() string { return "PING/v4" } +func (req *pingV4) kind() byte { return p_pingV4 } func (req *pingV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { @@ -767,10 +885,12 @@ func (req *pingV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromK func (req *pingV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []byte) { // Reply. - t.send(from, fromID, p_pongV4, &pongV4{ + seq, _ := rlp.EncodeToBytes(t.localNode.Node().Seq()) + t.send(from, fromID, &pongV4{ To: makeEndpoint(from, req.From.TCP), ReplyTok: mac, Expiration: uint64(time.Now().Add(expiration).Unix()), + Rest: []rlp.RawValue{seq}, }) // Ping back if our last pong on file is too far in the past. @@ -788,13 +908,16 @@ func (req *pingV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []by t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) } -func (req *pingV4) name() string { return "PING/v4" } +// PONG/v4 + +func (req *pongV4) name() string { return "PONG/v4" } +func (req *pongV4) kind() byte { return p_pongV4 } func (req *pongV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - if !t.handleReply(fromID, from.IP, p_pongV4, req) { + if !t.handleReply(fromID, from.IP, req) { return errUnsolicitedReply } return nil @@ -805,13 +928,16 @@ func (req *pongV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []by t.db.UpdateLastPongReceived(fromID, from.IP, time.Now()) } -func (req *pongV4) name() string { return "PONG/v4" } +// FINDNODE/v4 + +func (req *findnodeV4) name() string { return "FINDNODE/v4" } +func (req *findnodeV4) kind() byte { return p_findnodeV4 } func (req *findnodeV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - if time.Since(t.db.LastPongReceived(fromID, from.IP)) > bondExpiration { + if !t.checkBond(fromID, from.IP) { // No endpoint proof pong exists, we don't process the packet. This prevents an // attack vector where the discovery protocol could be used to amplify traffic in a // DDOS attack. A malicious actor would send a findnode request with the IP address @@ -839,23 +965,26 @@ func (req *findnodeV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac p.Nodes = append(p.Nodes, nodeToRPC(n)) } if len(p.Nodes) == maxNeighbors { - t.send(from, fromID, p_neighborsV4, &p) + t.send(from, fromID, &p) p.Nodes = p.Nodes[:0] sent = true } } if len(p.Nodes) > 0 || !sent { - t.send(from, fromID, p_neighborsV4, &p) + t.send(from, fromID, &p) } } -func (req *findnodeV4) name() string { return "FINDNODE/v4" } +// NEIGHBORS/v4 + +func (req *neighborsV4) name() string { return "NEIGHBORS/v4" } +func (req *neighborsV4) kind() byte { return p_neighborsV4 } func (req *neighborsV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - if !t.handleReply(fromID, from.IP, p_neighborsV4, req) { + if !t.handleReply(fromID, from.IP, req) { return errUnsolicitedReply } return nil @@ -864,8 +993,39 @@ func (req *neighborsV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, func (req *neighborsV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []byte) { } -func (req *neighborsV4) name() string { return "NEIGHBORS/v4" } +// ENRREQUEST/v4 -func expired(ts uint64) bool { - return time.Unix(int64(ts), 0).Before(time.Now()) +func (req *enrRequestV4) name() string { return "ENRREQUEST/v4" } +func (req *enrRequestV4) kind() byte { return p_enrRequestV4 } + +func (req *enrRequestV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { + if expired(req.Expiration) { + return errExpired + } + if !t.checkBond(fromID, from.IP) { + return errUnknownNode + } + return nil +} + +func (req *enrRequestV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []byte) { + t.send(from, fromID, &enrResponseV4{ + ReplyTok: mac, + Record: *t.localNode.Node().Record(), + }) +} + +// ENRRESPONSE/v4 + +func (req *enrResponseV4) name() string { return "ENRRESPONSE/v4" } +func (req *enrResponseV4) kind() byte { return p_enrResponseV4 } + +func (req *enrResponseV4) preverify(t *UDPv4, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { + if !t.handleReply(fromID, from.IP, req) { + return errUnsolicitedReply + } + return nil +} + +func (req *enrResponseV4) handle(t *UDPv4, from *net.UDPAddr, fromID enode.ID, mac []byte) { } diff --git a/p2p/discover/v4_udp_lookup_test.go b/p2p/discover/v4_udp_lookup_test.go index 7e12aa4986..bc1cdfb089 100644 --- a/p2p/discover/v4_udp_lookup_test.go +++ b/p2p/discover/v4_udp_lookup_test.go @@ -54,11 +54,11 @@ func TestUDPv4_Lookup(t *testing.T) { n, key := lookupTestnet.nodeByAddr(to) switch p.(type) { case *pingV4: - test.packetInFrom(nil, key, to, p_pongV4, &pongV4{Expiration: futureExp, ReplyTok: hash}) + test.packetInFrom(nil, key, to, &pongV4{Expiration: futureExp, ReplyTok: hash}) case *findnodeV4: dist := enode.LogDist(n.ID(), lookupTestnet.target.id()) nodes := lookupTestnet.nodesAtDistance(dist - 1) - test.packetInFrom(nil, key, to, p_neighborsV4, &neighborsV4{Expiration: futureExp, Nodes: nodes}) + test.packetInFrom(nil, key, to, &neighborsV4{Expiration: futureExp, Nodes: nodes}) } }) } diff --git a/p2p/discover/v4_udp_test.go b/p2p/discover/v4_udp_test.go index 0aa4c01e5e..9d7badea19 100644 --- a/p2p/discover/v4_udp_test.go +++ b/p2p/discover/v4_udp_test.go @@ -37,6 +37,7 @@ import ( "github.com/ethereum/go-ethereum/internal/testlog" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/rlp" ) @@ -91,19 +92,19 @@ func (test *udpTest) close() { } // handles a packet as if it had been sent to the transport. -func (test *udpTest) packetIn(wantError error, ptype byte, data packetV4) { +func (test *udpTest) packetIn(wantError error, data packetV4) { test.t.Helper() - test.packetInFrom(wantError, test.remotekey, test.remoteaddr, ptype, data) + test.packetInFrom(wantError, test.remotekey, test.remoteaddr, data) } // handles a packet as if it had been sent to the transport by the key/endpoint. -func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, ptype byte, data packetV4) { +func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, data packetV4) { test.t.Helper() - enc, _, err := test.udp.encode(key, ptype, data) + enc, _, err := test.udp.encode(key, data) if err != nil { - test.t.Errorf("packet (%d) encode error: %v", ptype, err) + test.t.Errorf("%s encode error: %v", data.name(), err) } test.sent = append(test.sent, enc) if err = test.udp.handlePacket(addr, enc); err != wantError { @@ -139,10 +140,10 @@ func TestUDPv4_packetErrors(t *testing.T) { test := newUDPTest(t) defer test.close() - test.packetIn(errExpired, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4}) - test.packetIn(errUnsolicitedReply, p_pongV4, &pongV4{ReplyTok: []byte{}, Expiration: futureExp}) - test.packetIn(errUnknownNode, p_findnodeV4, &findnodeV4{Expiration: futureExp}) - test.packetIn(errUnsolicitedReply, p_neighborsV4, &neighborsV4{Expiration: futureExp}) + test.packetIn(errExpired, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4}) + test.packetIn(errUnsolicitedReply, &pongV4{ReplyTok: []byte{}, Expiration: futureExp}) + test.packetIn(errUnknownNode, &findnodeV4{Expiration: futureExp}) + test.packetIn(errUnsolicitedReply, &neighborsV4{Expiration: futureExp}) } func TestUDPv4_pingTimeout(t *testing.T) { @@ -153,11 +154,21 @@ func TestUDPv4_pingTimeout(t *testing.T) { key := newkey() toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222} node := enode.NewV4(&key.PublicKey, toaddr.IP, 0, toaddr.Port) - if err := test.udp.ping(node); err != errTimeout { + if _, err := test.udp.ping(node); err != errTimeout { t.Error("expected timeout error, got", err) } } +type testPacket byte + +func (req testPacket) kind() byte { return byte(req) } +func (req testPacket) name() string { return "" } +func (req testPacket) preverify(*UDPv4, *net.UDPAddr, enode.ID, encPubkey) error { + return nil +} +func (req testPacket) handle(*UDPv4, *net.UDPAddr, enode.ID, []byte) { +} + func TestUDPv4_responseTimeouts(t *testing.T) { t.Parallel() test := newUDPTest(t) @@ -192,7 +203,7 @@ func TestUDPv4_responseTimeouts(t *testing.T) { p.errc = nilErr test.udp.addReplyMatcher <- p time.AfterFunc(randomDuration(60*time.Millisecond), func() { - if !test.udp.handleReply(p.from, p.ip, p.ptype, nil) { + if !test.udp.handleReply(p.from, p.ip, testPacket(p.ptype)) { t.Logf("not matched: %v", p) } }) @@ -277,7 +288,7 @@ func TestUDPv4_findnode(t *testing.T) { // check that closest neighbors are returned. expected := test.table.closest(testTarget.id(), bucketSize, true) - test.packetIn(nil, p_findnodeV4, &findnodeV4{Target: testTarget, Expiration: futureExp}) + test.packetIn(nil, &findnodeV4{Target: testTarget, Expiration: futureExp}) waitNeighbors := func(want []*node) { test.waitPacketOut(func(p *neighborsV4, to *net.UDPAddr, hash []byte) { if len(p.Nodes) != len(want) { @@ -340,8 +351,8 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) { for i := range list { rpclist[i] = nodeToRPC(list[i]) } - test.packetIn(nil, p_neighborsV4, &neighborsV4{Expiration: futureExp, Nodes: rpclist[:2]}) - test.packetIn(nil, p_neighborsV4, &neighborsV4{Expiration: futureExp, Nodes: rpclist[2:]}) + test.packetIn(nil, &neighborsV4{Expiration: futureExp, Nodes: rpclist[:2]}) + test.packetIn(nil, &neighborsV4{Expiration: futureExp, Nodes: rpclist[2:]}) // check that the sent neighbors are all returned by findnode select { @@ -357,6 +368,7 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) { } } +// This test checks that reply matching of pong verifies the ping hash. func TestUDPv4_pingMatch(t *testing.T) { test := newUDPTest(t) defer test.close() @@ -364,22 +376,23 @@ func TestUDPv4_pingMatch(t *testing.T) { randToken := make([]byte, 32) crand.Read(randToken) - test.packetIn(nil, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) + test.packetIn(nil, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) test.waitPacketOut(func(*pongV4, *net.UDPAddr, []byte) {}) test.waitPacketOut(func(*pingV4, *net.UDPAddr, []byte) {}) - test.packetIn(errUnsolicitedReply, p_pongV4, &pongV4{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp}) + test.packetIn(errUnsolicitedReply, &pongV4{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp}) } +// This test checks that reply matching of pong verifies the sender IP address. func TestUDPv4_pingMatchIP(t *testing.T) { test := newUDPTest(t) defer test.close() - test.packetIn(nil, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) + test.packetIn(nil, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) test.waitPacketOut(func(*pongV4, *net.UDPAddr, []byte) {}) test.waitPacketOut(func(p *pingV4, to *net.UDPAddr, hash []byte) { wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000} - test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, p_pongV4, &pongV4{ + test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &pongV4{ ReplyTok: hash, To: testLocalAnnounced, Expiration: futureExp, @@ -394,9 +407,9 @@ func TestUDPv4_successfulPing(t *testing.T) { defer test.close() // The remote side sends a ping packet to initiate the exchange. - go test.packetIn(nil, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) + go test.packetIn(nil, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) - // the ping is replied to. + // The ping is replied to. test.waitPacketOut(func(p *pongV4, to *net.UDPAddr, hash []byte) { pinghash := test.sent[0][:macSize] if !bytes.Equal(p.ReplyTok, pinghash) { @@ -413,7 +426,7 @@ func TestUDPv4_successfulPing(t *testing.T) { } }) - // remote is unknown, the table pings back. + // Remote is unknown, the table pings back. test.waitPacketOut(func(p *pingV4, to *net.UDPAddr, hash []byte) { if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) { t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint()) @@ -427,10 +440,10 @@ func TestUDPv4_successfulPing(t *testing.T) { if !reflect.DeepEqual(p.To, wantTo) { t.Errorf("got ping.To %v, want %v", p.To, wantTo) } - test.packetIn(nil, p_pongV4, &pongV4{ReplyTok: hash, Expiration: futureExp}) + test.packetIn(nil, &pongV4{ReplyTok: hash, Expiration: futureExp}) }) - // the node should be added to the table shortly after getting the + // The node should be added to the table shortly after getting the // pong packet. select { case n := <-added: @@ -452,6 +465,45 @@ func TestUDPv4_successfulPing(t *testing.T) { } } +// This test checks that EIP-868 requests work. +func TestUDPv4_EIP868(t *testing.T) { + test := newUDPTest(t) + defer test.close() + + test.udp.localNode.Set(enr.WithEntry("foo", "bar")) + wantNode := test.udp.localNode.Node() + + // ENR requests aren't allowed before endpoint proof. + test.packetIn(errUnknownNode, &enrRequestV4{Expiration: futureExp}) + + // Perform endpoint proof and check for sequence number in packet tail. + test.packetIn(nil, &pingV4{Expiration: futureExp}) + test.waitPacketOut(func(p *pongV4, addr *net.UDPAddr, hash []byte) { + if seq := seqFromTail(p.Rest); seq != wantNode.Seq() { + t.Errorf("wrong sequence number in pong: %d, want %d", seq, wantNode.Seq()) + } + }) + test.waitPacketOut(func(p *pingV4, addr *net.UDPAddr, hash []byte) { + if seq := seqFromTail(p.Rest); seq != wantNode.Seq() { + t.Errorf("wrong sequence number in ping: %d, want %d", seq, wantNode.Seq()) + } + test.packetIn(nil, &pongV4{Expiration: futureExp, ReplyTok: hash}) + }) + + // Request should work now. + test.packetIn(nil, &enrRequestV4{Expiration: futureExp}) + test.waitPacketOut(func(p *enrResponseV4, addr *net.UDPAddr, hash []byte) { + n, err := enode.New(enode.ValidSchemes, &p.Record) + if err != nil { + t.Fatalf("invalid record: %v", err) + } + if !reflect.DeepEqual(n, wantNode) { + t.Fatalf("wrong node in enrResponse: %v", n) + } + }) +} + +// EIP-8 test vectors. var testPackets = []struct { input string wantPacket interface{} From 9b0d1b9ab2447f2e7b876cff9905294a1208565b Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Wed, 15 May 2019 12:22:32 +0200 Subject: [PATCH 14/34] swarm/metrics: collect metrics on datadir disk usage (#19576) --- swarm/metrics/flags.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/swarm/metrics/flags.go b/swarm/metrics/flags.go index 3e7918b168..39859a7b2d 100644 --- a/swarm/metrics/flags.go +++ b/swarm/metrics/flags.go @@ -17,6 +17,8 @@ package metrics import ( + "os" + "path/filepath" "time" "github.com/ethereum/go-ethereum/cmd/utils" @@ -89,11 +91,15 @@ func Setup(ctx *cli.Context) { password = ctx.GlobalString(MetricsInfluxDBPasswordFlag.Name) enableExport = ctx.GlobalBool(MetricsEnableInfluxDBExportFlag.Name) enableAccountingExport = ctx.GlobalBool(MetricsEnableInfluxDBAccountingExportFlag.Name) + datadir = ctx.GlobalString("datadir") ) // Start system runtime metrics collection go gethmetrics.CollectProcessMetrics(4 * time.Second) + // Start collecting disk metrics + go datadirDiskUsage(datadir, 4*time.Second) + gethmetrics.RegisterRuntimeMemStats(metrics.DefaultRegistry) go gethmetrics.CaptureRuntimeMemStats(metrics.DefaultRegistry, 4*time.Second) @@ -110,3 +116,28 @@ func Setup(ctx *cli.Context) { } } } + +func datadirDiskUsage(path string, d time.Duration) { + for range time.Tick(d) { + bytes, err := dirSize(path) + if err != nil { + log.Warn("cannot get disk space", "err", err) + } + + metrics.GetOrRegisterGauge("datadir.usage", nil).Update(bytes) + } +} + +func dirSize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} From 4b9c3bd39a3df06600339ec4b06060cce30b6042 Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Wed, 15 May 2019 15:06:57 +0200 Subject: [PATCH 15/34] swarm/storage: disable open tracing on indices (#19578) --- swarm/storage/localstore/subscription_pull.go | 15 --------------- swarm/storage/localstore/subscription_push.go | 14 -------------- 2 files changed, 29 deletions(-) diff --git a/swarm/storage/localstore/subscription_pull.go b/swarm/storage/localstore/subscription_pull.go index dd07add535..ce539924b4 100644 --- a/swarm/storage/localstore/subscription_pull.go +++ b/swarm/storage/localstore/subscription_pull.go @@ -26,9 +26,6 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/swarm/chunk" "github.com/ethereum/go-ethereum/swarm/shed" - "github.com/ethereum/go-ethereum/swarm/spancontext" - "github.com/opentracing/opentracing-go" - olog "github.com/opentracing/opentracing-go/log" "github.com/syndtr/goleveldb/leveldb" ) @@ -88,9 +85,6 @@ func (db *DB) SubscribePull(ctx context.Context, bin uint8, since, until uint64) // - context is done metrics.GetOrRegisterCounter(metricName+".iter", nil).Inc(1) - ctx, sp := spancontext.StartSpan(ctx, metricName+".iter") - sp.LogFields(olog.Int("bin", int(bin)), olog.Uint64("since", since), olog.Uint64("until", until)) - iterStart := time.Now() var count int err := db.pullIndex.Iterate(func(item shed.Item) (stop bool, err error) { @@ -131,15 +125,6 @@ func (db *DB) SubscribePull(ctx context.Context, bin uint8, since, until uint64) totalTimeMetric(metricName+".iter", iterStart) - sp.FinishWithOptions(opentracing.FinishOptions{ - LogRecords: []opentracing.LogRecord{ - { - Timestamp: time.Now(), - Fields: []olog.Field{olog.Int("count", count)}, - }, - }, - }) - if err != nil { if err == errStopSubscription { // stop subscription without any errors diff --git a/swarm/storage/localstore/subscription_push.go b/swarm/storage/localstore/subscription_push.go index f2463af2ab..c43a751f41 100644 --- a/swarm/storage/localstore/subscription_push.go +++ b/swarm/storage/localstore/subscription_push.go @@ -25,9 +25,6 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/swarm/chunk" "github.com/ethereum/go-ethereum/swarm/shed" - "github.com/ethereum/go-ethereum/swarm/spancontext" - "github.com/opentracing/opentracing-go" - olog "github.com/opentracing/opentracing-go/log" ) // SubscribePush returns a channel that provides storage chunks with ordering from push syncing index. @@ -68,8 +65,6 @@ func (db *DB) SubscribePush(ctx context.Context) (c <-chan chunk.Chunk, stop fun // - context is done metrics.GetOrRegisterCounter(metricName+".iter", nil).Inc(1) - ctx, sp := spancontext.StartSpan(ctx, metricName+".iter") - iterStart := time.Now() var count int err := db.pushIndex.Iterate(func(item shed.Item) (stop bool, err error) { @@ -106,15 +101,6 @@ func (db *DB) SubscribePush(ctx context.Context) (c <-chan chunk.Chunk, stop fun totalTimeMetric(metricName+".iter", iterStart) - sp.FinishWithOptions(opentracing.FinishOptions{ - LogRecords: []opentracing.LogRecord{ - { - Timestamp: time.Now(), - Fields: []olog.Field{olog.Int("count", count)}, - }, - }, - }) - if err != nil { metrics.GetOrRegisterCounter(metricName+".iter.error", nil).Inc(1) log.Error("localstore push subscription iteration", "err", err) From b548b5aeb00dd51f4f2d982f60d9dec5af8615e8 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 15 May 2019 11:11:17 -0400 Subject: [PATCH 16/34] p2p/discover: fix crash in Resolve (#19579) --- p2p/discover/v4_udp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go index b0b0053a7a..b3569b6719 100644 --- a/p2p/discover/v4_udp.go +++ b/p2p/discover/v4_udp.go @@ -426,11 +426,11 @@ func (t *UDPv4) Resolve(n *enode.Node) *enode.Node { } } // Otherwise perform a network lookup. - var key *enode.Secp256k1 - if n.Load(key) != nil { + var key enode.Secp256k1 + if n.Load(&key) != nil { return n // no secp256k1 key } - result := t.LookupPubkey((*ecdsa.PublicKey)(key)) + result := t.LookupPubkey((*ecdsa.PublicKey)(&key)) for _, rn := range result { if rn.ID() == n.ID() { if rn, err := t.requestENR(rn); err == nil { From 0c5f8c078abca7dc5954e30f307495a5c41c5f6c Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Wed, 15 May 2019 21:26:07 +0200 Subject: [PATCH 17/34] accounts,signer: better support for EIP-191 intended validator (#19523) --- accounts/accounts.go | 2 +- signer/core/signed_data.go | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index afeb412fe3..bf5190ad98 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -36,7 +36,7 @@ type Account struct { } const ( - MimetypeTextWithValidator = "text/validator" + MimetypeDataWithValidator = "data/validator" MimetypeTypedData = "data/typed" MimetypeClique = "application/x-clique-header" MimetypeTextPlain = "text/plain" diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index d264cbaa08..9dfd7b3f69 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -46,8 +46,8 @@ type SigFormat struct { } var ( - TextValidator = SigFormat{ - accounts.MimetypeTextWithValidator, + IntendedValidator = SigFormat{ + accounts.MimetypeDataWithValidator, 0x00, } DataTyped = SigFormat{ @@ -191,7 +191,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType } switch mediaType { - case TextValidator.Mime: + case IntendedValidator.Mime: // Data with an intended validator validatorData, err := UnmarshalValidatorData(data) if err != nil { @@ -200,9 +200,24 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType sighash, msg := SignTextValidator(validatorData) message := []*NameValueType{ { - Name: "message", - Typ: "text", - Value: msg, + Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", + Typ: "description", + Value: "", + }, + { + Name: "Intended validator address", + Typ: "address", + Value: validatorData.Address.String(), + }, + { + Name: "Application-specific data", + Typ: "hexdata", + Value: validatorData.Message, + }, + { + Name: "Full message for signing", + Typ: "hexdata", + Value: fmt.Sprintf("0x%x", msg), }, } req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash} @@ -275,7 +290,6 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType // hash = keccak256("\x19\x00"${address}${data}). func SignTextValidator(validatorData ValidatorData) (hexutil.Bytes, string) { msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message)) - fmt.Printf("SignTextValidator:%s\n", msg) return crypto.Keccak256([]byte(msg)), msg } From 006c21efc7af8bdf04d003ef256d8e2eb30006bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 8 Mar 2019 15:56:20 +0200 Subject: [PATCH 18/34] cmd, core, eth, les, node: chain freezer on top of db rework --- cmd/geth/chaincmd.go | 7 +- cmd/geth/main.go | 1 + cmd/geth/usage.go | 1 + cmd/utils/flags.go | 9 +- core/rawdb/accessors_chain.go | 82 ++++++--- core/rawdb/accessors_indexes.go | 4 +- core/rawdb/database.go | 57 ++++++- core/rawdb/freezer.go | 276 +++++++++++++++++++++++++++++++ core/rawdb/freezer_table.go | 284 ++++++++++++++++++++++++++++++++ core/rawdb/table.go | 6 + eth/backend.go | 2 +- eth/config.go | 1 + ethdb/database.go | 15 +- node/node.go | 20 +++ node/service.go | 25 ++- 15 files changed, 755 insertions(+), 35 deletions(-) create mode 100644 core/rawdb/freezer.go create mode 100644 core/rawdb/freezer_table.go diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 582f0b768e..809f5cf4a1 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -368,9 +368,12 @@ func exportPreimages(ctx *cli.Context) error { func copyDb(ctx *cli.Context) error { // Ensure we have a source chain directory to copy - if len(ctx.Args()) != 1 { + if len(ctx.Args()) < 1 { utils.Fatalf("Source chaindata directory path argument missing") } + if len(ctx.Args()) < 2 { + utils.Fatalf("Source ancient chain directory path argument missing") + } // Initialize a new chain for the running node to sync into stack := makeFullNode(ctx) defer stack.Close() @@ -385,7 +388,7 @@ func copyDb(ctx *cli.Context) error { dl := downloader.New(0, chainDb, syncBloom, new(event.TypeMux), chain, nil, nil) // Create a source peer to satisfy downloader requests from - db, err := rawdb.NewLevelDBDatabase(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name)/2, 256, "") + db, err := rawdb.NewLevelDBDatabaseWithFreezer(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name)/2, 256, ctx.Args().Get(1), "") if err != nil { return err } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 838029333e..dc63f23026 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -62,6 +62,7 @@ var ( utils.BootnodesV4Flag, utils.BootnodesV5Flag, utils.DataDirFlag, + utils.AncientFlag, utils.KeyStoreDirFlag, utils.ExternalSignerFlag, utils.NoUSBFlag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 7ec1ab03f5..67b0027f25 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -69,6 +69,7 @@ var AppHelpFlagGroups = []flagGroup{ Flags: []cli.Flag{ configFileFlag, utils.DataDirFlag, + utils.AncientFlag, utils.KeyStoreDirFlag, utils.NoUSBFlag, utils.NetworkIdFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 2dc45cbba2..c40da85b05 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -117,6 +117,10 @@ var ( Usage: "Data directory for the databases and keystore", Value: DirectoryString{node.DefaultDataDir()}, } + AncientFlag = DirectoryFlag{ + Name: "datadir.ancient", + Usage: "Data directory for ancient chain segments (default = inside chaindata)", + } KeyStoreDirFlag = DirectoryFlag{ Name: "keystore", Usage: "Directory for the keystore (default = inside the datadir)", @@ -1378,6 +1382,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *eth.Config) { cfg.DatabaseCache = ctx.GlobalInt(CacheFlag.Name) * ctx.GlobalInt(CacheDatabaseFlag.Name) / 100 } cfg.DatabaseHandles = makeDatabaseHandles() + if ctx.GlobalIsSet(AncientFlag.Name) { + cfg.DatabaseFreezer = ctx.GlobalString(AncientFlag.Name) + } if gcmode := ctx.GlobalString(GCModeFlag.Name); gcmode != "full" && gcmode != "archive" { Fatalf("--%s must be either 'full' or 'archive'", GCModeFlag.Name) @@ -1566,7 +1573,7 @@ func MakeChainDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database { if ctx.GlobalString(SyncModeFlag.Name) == "light" { name = "lightchaindata" } - chainDb, err := stack.OpenDatabase(name, cache, handles, "") + chainDb, err := stack.OpenDatabaseWithFreezer(name, cache, handles, "", "") if err != nil { Fatalf("Could not open database: %v", err) } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index cc0591a4ca..103f18f78c 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -30,8 +30,11 @@ import ( ) // ReadCanonicalHash retrieves the hash assigned to a canonical block number. -func ReadCanonicalHash(db ethdb.Reader, number uint64) common.Hash { - data, _ := db.Get(headerHashKey(number)) +func ReadCanonicalHash(db ethdb.AncientReader, number uint64) common.Hash { + data, _ := db.Ancient("hashes", number) + if len(data) == 0 { + data, _ = db.Get(headerHashKey(number)) + } if len(data) == 0 { return common.Hash{} } @@ -52,6 +55,24 @@ func DeleteCanonicalHash(db ethdb.Writer, number uint64) { } } +// readAllHashes retrieves all the hashes assigned to blocks at a certain heights, +// both canonical and reorged forks included. +// +// This method is a helper for the chain reader. It should never be exposed to the +// outside world. +func readAllHashes(db ethdb.Iteratee, number uint64) []common.Hash { + prefix := headerKeyPrefix(number) + + hashes := make([]common.Hash, 0, 1) + it := db.NewIteratorWithPrefix(prefix) + for it.Next() { + if key := it.Key(); len(key) == len(prefix)+32 { + hashes = append(hashes, common.BytesToHash(key[len(key)-32:])) + } + } + return hashes +} + // ReadHeaderNumber returns the header number assigned to a hash. func ReadHeaderNumber(db ethdb.Reader, hash common.Hash) *uint64 { data, _ := db.Get(headerNumberKey(hash)) @@ -129,13 +150,19 @@ func WriteFastTrieProgress(db ethdb.Writer, count uint64) { } // ReadHeaderRLP retrieves a block header in its raw RLP database encoding. -func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(headerKey(number, hash)) +func ReadHeaderRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient("headers", number) + if len(data) == 0 { + data, _ = db.Get(headerKey(number, hash)) + } return data } // HasHeader verifies the existence of a block header corresponding to the hash. -func HasHeader(db ethdb.Reader, hash common.Hash, number uint64) bool { +func HasHeader(db ethdb.AncientReader, hash common.Hash, number uint64) bool { + if has, err := db.Ancient("hashes", number); err == nil && common.BytesToHash(has) == hash { + return true + } if has, err := db.Has(headerKey(number, hash)); !has || err != nil { return false } @@ -143,7 +170,7 @@ func HasHeader(db ethdb.Reader, hash common.Hash, number uint64) bool { } // ReadHeader retrieves the block header corresponding to the hash. -func ReadHeader(db ethdb.Reader, hash common.Hash, number uint64) *types.Header { +func ReadHeader(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Header { data := ReadHeaderRLP(db, hash, number) if len(data) == 0 { return nil @@ -197,8 +224,11 @@ func deleteHeaderWithoutNumber(db ethdb.Writer, hash common.Hash, number uint64) } // ReadBodyRLP retrieves the block body (transactions and uncles) in RLP encoding. -func ReadBodyRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(blockBodyKey(number, hash)) +func ReadBodyRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient("bodies", number) + if len(data) == 0 { + data, _ = db.Get(blockBodyKey(number, hash)) + } return data } @@ -210,7 +240,10 @@ func WriteBodyRLP(db ethdb.Writer, hash common.Hash, number uint64, rlp rlp.RawV } // HasBody verifies the existence of a block body corresponding to the hash. -func HasBody(db ethdb.Reader, hash common.Hash, number uint64) bool { +func HasBody(db ethdb.AncientReader, hash common.Hash, number uint64) bool { + if has, err := db.Ancient("hashes", number); err == nil && common.BytesToHash(has) == hash { + return true + } if has, err := db.Has(blockBodyKey(number, hash)); !has || err != nil { return false } @@ -218,7 +251,7 @@ func HasBody(db ethdb.Reader, hash common.Hash, number uint64) bool { } // ReadBody retrieves the block body corresponding to the hash. -func ReadBody(db ethdb.Reader, hash common.Hash, number uint64) *types.Body { +func ReadBody(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Body { data := ReadBodyRLP(db, hash, number) if len(data) == 0 { return nil @@ -248,13 +281,16 @@ func DeleteBody(db ethdb.Writer, hash common.Hash, number uint64) { } // ReadTdRLP retrieves a block's total difficulty corresponding to the hash in RLP encoding. -func ReadTdRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(headerTDKey(number, hash)) +func ReadTdRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient("diffs", number) + if len(data) == 0 { + data, _ = db.Get(headerTDKey(number, hash)) + } return data } // ReadTd retrieves a block's total difficulty corresponding to the hash. -func ReadTd(db ethdb.Reader, hash common.Hash, number uint64) *big.Int { +func ReadTd(db ethdb.AncientReader, hash common.Hash, number uint64) *big.Int { data := ReadTdRLP(db, hash, number) if len(data) == 0 { return nil @@ -287,7 +323,10 @@ func DeleteTd(db ethdb.Writer, hash common.Hash, number uint64) { // HasReceipts verifies the existence of all the transaction receipts belonging // to a block. -func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool { +func HasReceipts(db ethdb.AncientReader, hash common.Hash, number uint64) bool { + if has, err := db.Ancient("hashes", number); err == nil && common.BytesToHash(has) == hash { + return true + } if has, err := db.Has(blockReceiptsKey(number, hash)); !has || err != nil { return false } @@ -295,15 +334,18 @@ func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool { } // ReadReceiptsRLP retrieves all the transaction receipts belonging to a block in RLP encoding. -func ReadReceiptsRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(blockReceiptsKey(number, hash)) +func ReadReceiptsRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient("receipts", number) + if len(data) == 0 { + data, _ = db.Get(blockReceiptsKey(number, hash)) + } return data } // ReadRawReceipts retrieves all the transaction receipts belonging to a block. // The receipt metadata fields are not guaranteed to be populated, so they // should not be used. Use ReadReceipts instead if the metadata is needed. -func ReadRawReceipts(db ethdb.Reader, hash common.Hash, number uint64) types.Receipts { +func ReadRawReceipts(db ethdb.AncientReader, hash common.Hash, number uint64) types.Receipts { // Retrieve the flattened receipt slice data := ReadReceiptsRLP(db, hash, number) if len(data) == 0 { @@ -329,7 +371,7 @@ func ReadRawReceipts(db ethdb.Reader, hash common.Hash, number uint64) types.Rec // The current implementation populates these metadata fields by reading the receipts' // corresponding block body, so if the block body is not found it will return nil even // if the receipt itself is stored. -func ReadReceipts(db ethdb.Reader, hash common.Hash, number uint64, config *params.ChainConfig) types.Receipts { +func ReadReceipts(db ethdb.AncientReader, hash common.Hash, number uint64, config *params.ChainConfig) types.Receipts { // We're deriving many fields from the block body, retrieve beside the receipt receipts := ReadRawReceipts(db, hash, number) if receipts == nil { @@ -377,7 +419,7 @@ func DeleteReceipts(db ethdb.Writer, hash common.Hash, number uint64) { // // Note, due to concurrent download of header and block body the header and thus // canonical hash can be stored in the database but the body data not (yet). -func ReadBlock(db ethdb.Reader, hash common.Hash, number uint64) *types.Block { +func ReadBlock(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Block { header := ReadHeader(db, hash, number) if header == nil { return nil @@ -413,7 +455,7 @@ func deleteBlockWithoutNumber(db ethdb.Writer, hash common.Hash, number uint64) } // FindCommonAncestor returns the last common ancestor of two block headers -func FindCommonAncestor(db ethdb.Reader, a, b *types.Header) *types.Header { +func FindCommonAncestor(db ethdb.AncientReader, a, b *types.Header) *types.Header { for bn := b.Number.Uint64(); a.Number.Uint64() > bn; { a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1) if a == nil { diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index 423145a760..666e3edff6 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -69,7 +69,7 @@ func DeleteTxLookupEntry(db ethdb.Writer, hash common.Hash) { // ReadTransaction retrieves a specific transaction from the database, along with // its added positional metadata. -func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, common.Hash, uint64, uint64) { +func ReadTransaction(db ethdb.AncientReader, hash common.Hash) (*types.Transaction, common.Hash, uint64, uint64) { blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { return nil, common.Hash{}, 0, 0 @@ -94,7 +94,7 @@ func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, com // ReadReceipt retrieves a specific transaction receipt from the database, along with // its added positional metadata. -func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { +func ReadReceipt(db ethdb.AncientReader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { // Retrieve the context of the receipt based on the transaction hash blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index b4c5dea708..0f994c3fd3 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -22,10 +22,44 @@ import ( "github.com/ethereum/go-ethereum/ethdb/memorydb" ) +// freezerdb is a databse wrapper that enabled freezer data retrievals. +type freezerdb struct { + ethdb.KeyValueStore + ethdb.Ancienter +} + +// nofreezedb is a database wrapper that disables freezer data retrievals. +type nofreezedb struct { + ethdb.KeyValueStore +} + +// Frozen returns nil as we don't have a backing chain freezer. +func (db *nofreezedb) Ancient(kind string, number uint64) ([]byte, error) { + return nil, errOutOfBounds +} + // NewDatabase creates a high level database on top of a given key-value data // store without a freezer moving immutable chain segments into cold storage. func NewDatabase(db ethdb.KeyValueStore) ethdb.Database { - return db + return &nofreezedb{ + KeyValueStore: db, + } +} + +// NewDatabaseWithFreezer creates a high level database on top of a given key- +// value data store with a freezer moving immutable chain segments into cold +// storage. +func NewDatabaseWithFreezer(db ethdb.KeyValueStore, freezer string, namespace string) (ethdb.Database, error) { + frdb, err := newFreezer(freezer, namespace) + if err != nil { + return nil, err + } + go frdb.freeze(db) + + return &freezerdb{ + KeyValueStore: db, + Ancienter: frdb, + }, nil } // NewMemoryDatabase creates an ephemeral in-memory key-value database without a @@ -34,9 +68,9 @@ func NewMemoryDatabase() ethdb.Database { return NewDatabase(memorydb.New()) } -// NewMemoryDatabaseWithCap creates an ephemeral in-memory key-value database with -// an initial starting capacity, but without a freezer moving immutable chain -// segments into cold storage. +// NewMemoryDatabaseWithCap creates an ephemeral in-memory key-value database +// with an initial starting capacity, but without a freezer moving immutable +// chain segments into cold storage. func NewMemoryDatabaseWithCap(size int) ethdb.Database { return NewDatabase(memorydb.NewWithCap(size)) } @@ -50,3 +84,18 @@ func NewLevelDBDatabase(file string, cache int, handles int, namespace string) ( } return NewDatabase(db), nil } + +// NewLevelDBDatabaseWithFreezer creates a persistent key-value database with a +// freezer moving immutable chain segments into cold storage. +func NewLevelDBDatabaseWithFreezer(file string, cache int, handles int, freezer string, namespace string) (ethdb.Database, error) { + kvdb, err := leveldb.New(file, cache, handles, namespace) + if err != nil { + return nil, err + } + frdb, err := NewDatabaseWithFreezer(kvdb, freezer, namespace) + if err != nil { + kvdb.Close() + return nil, err + } + return frdb, nil +} diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go new file mode 100644 index 0000000000..4f227e3b78 --- /dev/null +++ b/core/rawdb/freezer.go @@ -0,0 +1,276 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "errors" + "fmt" + "math" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" +) + +// errUnknownTable is returned if the user attempts to read from a table that is +// not tracked by the freezer. +var errUnknownTable = errors.New("unknown table") + +const ( + // freezerRecheckInterval is the frequency to check the key-value database for + // chain progression that might permit new blocks to be frozen into immutable + // storage. + freezerRecheckInterval = time.Minute + + // freezerBlockGraduation is the number of confirmations a block must achieve + // before it becomes elligible for chain freezing. This must exceed any chain + // reorg depth, since the freezer also deletes all block siblings. + freezerBlockGraduation = 60000 + + // freezerBatchLimit is the maximum number of blocks to freeze in one batch + // before doing an fsync and deleting it from the key-value store. + freezerBatchLimit = 30000 +) + +// freezer is an memory mapped append-only database to store immutable chain data +// into flat files: +// +// - The append only nature ensures that disk writes are minimized. +// - The memory mapping ensures we can max out system memory for caching without +// reserving it for go-ethereum. This would also reduce the memory requirements +// of Geth, and thus also GC overhead. +type freezer struct { + tables map[string]*freezerTable // Data tables for storing everything + frozen uint64 // Number of blocks already frozen +} + +// newFreezer creates a chain freezer that moves ancient chain data into +// append-only flat file containers. +func newFreezer(datadir string, namespace string) (*freezer, error) { + // Create the initial freezer object + var ( + readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) + writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) + ) + // Open all the supported data tables + freezer := &freezer{ + tables: make(map[string]*freezerTable), + } + for _, name := range []string{"hashes", "headers", "bodies", "receipts", "diffs"} { + table, err := newTable(datadir, name, readMeter, writeMeter) + if err != nil { + for _, table := range freezer.tables { + table.Close() + } + return nil, err + } + freezer.tables[name] = table + } + // Truncate all data tables to the same length + freezer.frozen = math.MaxUint64 + for _, table := range freezer.tables { + if freezer.frozen > table.items { + freezer.frozen = table.items + } + } + for _, table := range freezer.tables { + if err := table.truncate(freezer.frozen); err != nil { + for _, table := range freezer.tables { + table.Close() + } + return nil, err + } + } + return freezer, nil +} + +// Close terminates the chain freezer, unmapping all the data files. +func (f *freezer) Close() error { + var errs []error + for _, table := range f.tables { + if err := table.Close(); err != nil { + errs = append(errs, err) + } + } + if errs != nil { + return fmt.Errorf("%v", errs) + } + return nil +} + +// sync flushes all data tables to disk. +func (f *freezer) sync() error { + var errs []error + for _, table := range f.tables { + if err := table.Sync(); err != nil { + errs = append(errs, err) + } + } + if errs != nil { + return fmt.Errorf("%v", errs) + } + return nil +} + +// Ancient retrieves an ancient binary blob from the append-only immutable files. +func (f *freezer) Ancient(kind string, number uint64) ([]byte, error) { + if table := f.tables[kind]; table != nil { + return table.Retrieve(number) + } + return nil, errUnknownTable +} + +// freeze is a background thread that periodically checks the blockchain for any +// import progress and moves ancient data from the fast database into the freezer. +// +// This functionality is deliberately broken off from block importing to avoid +// incurring additional data shuffling delays on block propagation. +func (f *freezer) freeze(db ethdb.KeyValueStore) { + nfdb := &nofreezedb{KeyValueStore: db} + + for { + // Retrieve the freezing threshold. In theory we're interested only in full + // blocks post-sync, but that would keep the live database enormous during + // dast sync. By picking the fast block, we still get to deep freeze all the + // final immutable data without having to wait for sync to finish. + hash := ReadHeadFastBlockHash(nfdb) + if hash == (common.Hash{}) { + log.Debug("Current fast block hash unavailable") // new chain, empty database + time.Sleep(freezerRecheckInterval) + continue + } + number := ReadHeaderNumber(nfdb, hash) + switch { + case number == nil: + log.Error("Current fast block number unavailable", "hash", hash) + time.Sleep(freezerRecheckInterval) + continue + + case *number < freezerBlockGraduation: + log.Debug("Current fast block not old enough", "number", *number, "hash", hash, "delay", freezerBlockGraduation) + time.Sleep(freezerRecheckInterval) + continue + + case *number-freezerBlockGraduation <= f.frozen: + log.Debug("Ancient blocks frozen already", "number", *number, "hash", hash, "frozen", f.frozen) + time.Sleep(freezerRecheckInterval) + continue + } + head := ReadHeader(nfdb, hash, *number) + if head == nil { + log.Error("Current fast block unavailable", "number", *number, "hash", hash) + time.Sleep(freezerRecheckInterval) + continue + } + // Seems we have data ready to be frozen, process in usable batches + limit := *number - freezerBlockGraduation + if limit-f.frozen > freezerBatchLimit { + limit = f.frozen + freezerBatchLimit + } + var ( + start = time.Now() + first = f.frozen + ancients = make([]common.Hash, 0, limit) + ) + for f.frozen < limit { + // Retrieves all the components of the canonical block + hash := ReadCanonicalHash(nfdb, f.frozen) + if hash == (common.Hash{}) { + log.Error("Canonical hash missing, can't freeze", "number", f.frozen) + break + } + header := ReadHeaderRLP(nfdb, hash, f.frozen) + if len(header) == 0 { + log.Error("Block header missing, can't freeze", "number", f.frozen, "hash", hash) + break + } + body := ReadBodyRLP(nfdb, hash, f.frozen) + if len(body) == 0 { + log.Error("Block body missing, can't freeze", "number", f.frozen, "hash", hash) + break + } + receipts := ReadReceiptsRLP(nfdb, hash, f.frozen) + if len(receipts) == 0 { + log.Error("Block receipts missing, can't freeze", "number", f.frozen, "hash", hash) + break + } + td := ReadTdRLP(nfdb, hash, f.frozen) + if len(td) == 0 { + log.Error("Total difficulty missing, can't freeze", "number", f.frozen, "hash", hash) + break + } + // Inject all the components into the relevant data tables + if err := f.tables["hashes"].Append(f.frozen, hash[:]); err != nil { + log.Error("Failed to deep freeze hash", "number", f.frozen, "hash", hash, "err", err) + break + } + if err := f.tables["headers"].Append(f.frozen, header); err != nil { + log.Error("Failed to deep freeze header", "number", f.frozen, "hash", hash, "err", err) + break + } + if err := f.tables["bodies"].Append(f.frozen, body); err != nil { + log.Error("Failed to deep freeze body", "number", f.frozen, "hash", hash, "err", err) + break + } + if err := f.tables["receipts"].Append(f.frozen, receipts); err != nil { + log.Error("Failed to deep freeze receipts", "number", f.frozen, "hash", hash, "err", err) + break + } + if err := f.tables["diffs"].Append(f.frozen, td); err != nil { + log.Error("Failed to deep freeze difficulty", "number", f.frozen, "hash", hash, "err", err) + break + } + log.Trace("Deep froze ancient block", "number", f.frozen, "hash", hash) + atomic.AddUint64(&f.frozen, 1) // Only modify atomically + ancients = append(ancients, hash) + } + // Batch of blocks have been frozen, flush them before wiping from leveldb + if err := f.sync(); err != nil { + log.Crit("Failed to flush frozen tables", "err", err) + } + // Wipe out all data from the active database + batch := db.NewBatch() + for number := first; number < f.frozen; number++ { + for _, hash := range readAllHashes(db, number) { + if hash == ancients[number-first] { + deleteBlockWithoutNumber(batch, hash, number) + } else { + DeleteBlock(batch, hash, number) + } + } + } + if err := batch.Write(); err != nil { + log.Crit("Failed to delete frozen items", "err", err) + } + // Log something friendly for the user + context := []interface{}{ + "blocks", f.frozen - first, "elapsed", common.PrettyDuration(time.Since(start)), "number", f.frozen - 1, + } + if n := len(ancients); n > 0 { + context = append(context, []interface{}{"hash", ancients[n-1]}...) + } + log.Info("Deep froze chain segment", context...) + + // Avoid database thrashing with tiny writes + if f.frozen-first < freezerBatchLimit { + time.Sleep(freezerRecheckInterval) + } + } +} diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go new file mode 100644 index 0000000000..546db0c656 --- /dev/null +++ b/core/rawdb/freezer_table.go @@ -0,0 +1,284 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/golang/snappy" +) + +var ( + // errClosed is returned if an operation attempts to read from or write to the + // freezer table after it has already been closed. + errClosed = errors.New("closed") + + // errOutOfBounds is returned if the item requested is not contained within the + // freezer table. + errOutOfBounds = errors.New("out of bounds") +) + +// freezerTable represents a single chained data table within the freezer (e.g. blocks). +// It consists of a data file (snappy encoded arbitrary data blobs) and an index +// file (uncompressed 64 bit indices into the data file). +type freezerTable struct { + content *os.File // File descriptor for the data content of the table + offsets *os.File // File descriptor for the index file of the table + + items uint64 // Number of items stored in the table + bytes uint64 // Number of content bytes stored in the table + + readMeter metrics.Meter // Meter for measuring the effective amount of data read + writeMeter metrics.Meter // Meter for measuring the effective amount of data written + + logger log.Logger // Logger with database path and table name ambedded + lock sync.RWMutex // Mutex protecting the data file descriptors +} + +// newTable opens a freezer table, creating the data and index files if they are +// non existent. Both files are truncated to the shortest common length to ensure +// they don't go out of sync. +func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter) (*freezerTable, error) { + // Ensure the containing directory exists and open the two data files + if err := os.MkdirAll(path, 0755); err != nil { + return nil, err + } + content, err := os.OpenFile(filepath.Join(path, name+".dat"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return nil, err + } + offsets, err := os.OpenFile(filepath.Join(path, name+".idx"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + content.Close() + return nil, err + } + // Create the table and repair any past inconsistency + tab := &freezerTable{ + content: content, + offsets: offsets, + readMeter: readMeter, + writeMeter: writeMeter, + logger: log.New("database", path, "table", name), + } + if err := tab.repair(); err != nil { + offsets.Close() + content.Close() + return nil, err + } + return tab, nil +} + +// repair cross checks the content and the offsets file and truncates them to +// be in sync with each other after a potential crash / data loss. +func (t *freezerTable) repair() error { + // Create a temporary offset buffer to init files with and read offsts into + offset := make([]byte, 8) + + // If we've just created the files, initialize the offsets with the 0 index + stat, err := t.offsets.Stat() + if err != nil { + return err + } + if stat.Size() == 0 { + if _, err := t.offsets.Write(offset); err != nil { + return err + } + } + // Ensure the offsets are a multiple of 8 bytes + if overflow := stat.Size() % 8; overflow != 0 { + t.offsets.Truncate(stat.Size() - overflow) // New file can't trigger this path + } + // Retrieve the file sizes and prepare for truncation + if stat, err = t.offsets.Stat(); err != nil { + return err + } + offsetsSize := stat.Size() + + if stat, err = t.content.Stat(); err != nil { + return err + } + contentSize := stat.Size() + + // Keep truncating both files until they come in sync + t.offsets.ReadAt(offset, offsetsSize-8) + contentExp := int64(binary.LittleEndian.Uint64(offset)) + + for contentExp != contentSize { + // Truncate the content file to the last offset pointer + if contentExp < contentSize { + t.logger.Warn("Truncating dangling content", "indexed", common.StorageSize(contentExp), "stored", common.StorageSize(contentSize)) + if err := t.content.Truncate(contentExp); err != nil { + return err + } + contentSize = contentExp + } + // Truncate the offsets to point within the content file + if contentExp > contentSize { + t.logger.Warn("Truncating dangling offsets", "indexed", common.StorageSize(contentExp), "stored", common.StorageSize(contentSize)) + if err := t.offsets.Truncate(offsetsSize - 8); err != nil { + return err + } + offsetsSize -= 8 + + t.offsets.ReadAt(offset, offsetsSize-8) + contentExp = int64(binary.LittleEndian.Uint64(offset)) + } + } + // Ensure all reparation changes have been written to disk + if err := t.offsets.Sync(); err != nil { + return err + } + if err := t.content.Sync(); err != nil { + return err + } + // Update the item and byte counters and return + t.items = uint64(offsetsSize/8 - 1) // last index points to the end of the data file + t.bytes = uint64(contentSize) + + t.logger.Debug("Chain freezer table opened", "items", t.items, "size", common.StorageSize(t.bytes)) + return nil +} + +// truncate discards any recent data above the provided threashold number. +func (t *freezerTable) truncate(items uint64) error { + // If out item count is corrent, don't do anything + if t.items <= items { + return nil + } + // Something's out of sync, truncate the table's offset index + t.logger.Warn("Truncating freezer table", "items", t.items, "limit", items) + if err := t.offsets.Truncate(int64(items+1) * 8); err != nil { + return err + } + // Calculate the new expected size of the data file and truncate it + offset := make([]byte, 8) + t.offsets.ReadAt(offset, int64(items)*8) + expected := binary.LittleEndian.Uint64(offset) + + if err := t.content.Truncate(int64(expected)); err != nil { + return err + } + // All data files truncated, set internal counters and return + t.items, t.bytes = items, expected + return nil +} + +// Close unmaps all active memory mapped regions. +func (t *freezerTable) Close() error { + t.lock.Lock() + defer t.lock.Unlock() + + var errs []error + if err := t.offsets.Close(); err != nil { + errs = append(errs, err) + } + t.offsets = nil + + if err := t.content.Close(); err != nil { + errs = append(errs, err) + } + t.content = nil + + if errs != nil { + return fmt.Errorf("%v", errs) + } + return nil +} + +// Append injects a binary blob at the end of the freezer table. The item index +// is a precautionary parameter to ensure data correctness, but the table will +// reject already existing data. +// +// Note, this method will *not* flush any data to disk so be sure to explicitly +// fsync before irreversibly deleting data from the database. +func (t *freezerTable) Append(item uint64, blob []byte) error { + // Ensure the table is still accessible + if t.offsets == nil || t.content == nil { + return errClosed + } + // Ensure only the next item can be written, nothing else + if t.items != item { + panic(fmt.Sprintf("appending unexpected item: want %d, have %d", t.items, item)) + } + // Encode the blob and write it into the data file + blob = snappy.Encode(nil, blob) + if _, err := t.content.Write(blob); err != nil { + return err + } + t.bytes += uint64(len(blob)) + + offset := make([]byte, 8) + binary.LittleEndian.PutUint64(offset, t.bytes) + if _, err := t.offsets.Write(offset); err != nil { + return err + } + t.items++ + + t.writeMeter.Mark(int64(len(blob) + 8)) // 8 = 1 x 8 byte offset + return nil +} + +// Retrieve looks up the data offset of an item with the given index and retrieves +// the raw binary blob from the data file. +func (t *freezerTable) Retrieve(item uint64) ([]byte, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + // Ensure the table and the item is accessible + if t.offsets == nil || t.content == nil { + return nil, errClosed + } + if t.items <= item { + return nil, errOutOfBounds + } + // Item reachable, retrieve the data content boundaries + offset := make([]byte, 8) + if _, err := t.offsets.ReadAt(offset, int64(item*8)); err != nil { + return nil, err + } + start := binary.LittleEndian.Uint64(offset) + + if _, err := t.offsets.ReadAt(offset, int64((item+1)*8)); err != nil { + return nil, err + } + end := binary.LittleEndian.Uint64(offset) + + // Retrieve the data itself, decompress and return + blob := make([]byte, end-start) + if _, err := t.content.ReadAt(blob, int64(start)); err != nil { + return nil, err + } + t.readMeter.Mark(int64(len(blob) + 16)) // 16 = 2 x 8 byte offset + return snappy.Decode(nil, blob) +} + +// Sync pushes any pending data from memory out to disk. This is an expensive +// operation, so use it with care. +func (t *freezerTable) Sync() error { + if err := t.offsets.Sync(); err != nil { + return err + } + return t.content.Sync() +} diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 0e50db7c90..0b5e08b207 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -50,6 +50,12 @@ func (t *table) Get(key []byte) ([]byte, error) { return t.db.Get(append([]byte(t.prefix), key...)) } +// Ancient is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) Ancient(kind string, number uint64) ([]byte, error) { + return t.db.Ancient(kind, number) +} + // Put inserts the given value into the database at a prefixed version of the // provided key. func (t *table) Put(key []byte, value []byte) error { diff --git a/eth/backend.go b/eth/backend.go index f696157763..6b9c98bf2a 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -120,7 +120,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { log.Info("Allocated trie memory caches", "clean", common.StorageSize(config.TrieCleanCache)*1024*1024, "dirty", common.StorageSize(config.TrieDirtyCache)*1024*1024) // Assemble the Ethereum object - chainDb, err := ctx.OpenDatabase("chaindata", config.DatabaseCache, config.DatabaseHandles, "eth/db/chaindata/") + chainDb, err := ctx.OpenDatabaseWithFreezer("chaindata", config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, "eth/db/chaindata/") if err != nil { return nil, err } diff --git a/eth/config.go b/eth/config.go index fbe6597b67..ccd5674a74 100644 --- a/eth/config.go +++ b/eth/config.go @@ -114,6 +114,7 @@ type Config struct { SkipBcVersionCheck bool `toml:"-"` DatabaseHandles int `toml:"-"` DatabaseCache int + DatabaseFreezer string TrieCleanCache int TrieDirtyCache int diff --git a/ethdb/database.go b/ethdb/database.go index bab99aed1f..764e304e3c 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -67,10 +67,23 @@ type KeyValueStore interface { io.Closer } +// Ancienter wraps the Ancient method for a backing immutable chain data store. +type Ancienter interface { + // Ancient retrieves an ancient binary blob from the append-only immutable files. + Ancient(kind string, number uint64) ([]byte, error) +} + +// AncientReader contains the methods required to access both key-value as well as +// immutable ancient data. +type AncientReader interface { + Reader + Ancienter +} + // Database contains all the methods required by the high level database to not // only access the key-value data store but also the chain freezer. type Database interface { - Reader + AncientReader Writer Batcher Iteratee diff --git a/node/node.go b/node/node.go index 78bb492f00..08daeeee0f 100644 --- a/node/node.go +++ b/node/node.go @@ -614,6 +614,26 @@ func (n *Node) OpenDatabase(name string, cache, handles int, namespace string) ( return rawdb.NewLevelDBDatabase(n.config.ResolvePath(name), cache, handles, namespace) } +// OpenDatabaseWithFreezer opens an existing database with the given name (or +// creates one if no previous can be found) from within the node's data directory, +// also attaching a chain freezer to it that moves ancient chain data from the +// database to immutable append-only files. If the node is an ephemeral one, a +// memory database is returned. +func (n *Node) OpenDatabaseWithFreezer(name string, cache, handles int, freezer, namespace string) (ethdb.Database, error) { + if n.config.DataDir == "" { + return rawdb.NewMemoryDatabase(), nil + } + root := n.config.ResolvePath(name) + + switch { + case freezer == "": + freezer = filepath.Join(root, "ancient") + case !filepath.IsAbs(freezer): + freezer = n.config.ResolvePath(freezer) + } + return rawdb.NewLevelDBDatabaseWithFreezer(root, cache, handles, freezer, namespace) +} + // ResolvePath returns the absolute path of a resource in the instance directory. func (n *Node) ResolvePath(x string) string { return n.config.ResolvePath(x) diff --git a/node/service.go b/node/service.go index 24f8097435..4dea00995c 100644 --- a/node/service.go +++ b/node/service.go @@ -17,6 +17,7 @@ package node import ( + "path/filepath" "reflect" "github.com/ethereum/go-ethereum/accounts" @@ -44,11 +45,27 @@ func (ctx *ServiceContext) OpenDatabase(name string, cache int, handles int, nam if ctx.config.DataDir == "" { return rawdb.NewMemoryDatabase(), nil } - db, err := rawdb.NewLevelDBDatabase(ctx.config.ResolvePath(name), cache, handles, namespace) - if err != nil { - return nil, err + return rawdb.NewLevelDBDatabase(ctx.config.ResolvePath(name), cache, handles, namespace) +} + +// OpenDatabaseWithFreezer opens an existing database with the given name (or +// creates one if no previous can be found) from within the node's data directory, +// also attaching a chain freezer to it that moves ancient chain data from the +// database to immutable append-only files. If the node is an ephemeral one, a +// memory database is returned. +func (ctx *ServiceContext) OpenDatabaseWithFreezer(name string, cache int, handles int, freezer string, namespace string) (ethdb.Database, error) { + if ctx.config.DataDir == "" { + return rawdb.NewMemoryDatabase(), nil } - return db, nil + root := ctx.config.ResolvePath(name) + + switch { + case freezer == "": + freezer = filepath.Join(root, "ancient") + case !filepath.IsAbs(freezer): + freezer = ctx.config.ResolvePath(freezer) + } + return rawdb.NewLevelDBDatabaseWithFreezer(root, cache, handles, freezer, namespace) } // ResolvePath resolves a user path into the data directory if that was relative From b69bdc2a4f8efab7cb99934652e2d9b2508c4544 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 26 Mar 2019 12:28:23 +0100 Subject: [PATCH 19/34] freezer: implement split files for data * freezer: implement split files for data * freezer: add tests * freezer: close old head-file when opening next * freezer: fix truncation * freezer: more testing around close/open * rawdb/freezer: address review concerns * freezer: fix minor review concerns * freezer: fix remaining concerns + testcases around truncation * freezer: docs * freezer: implement multithreading * core/rawdb: fix freezer nitpicks + change offsets to uint32 * freezer: preopen files, simplify lock constructs * freezer: delete files during truncation --- core/rawdb/freezer_table.go | 401 ++++++++++++++++++------ core/rawdb/freezer_table_test.go | 511 +++++++++++++++++++++++++++++++ 2 files changed, 816 insertions(+), 96 deletions(-) create mode 100644 core/rawdb/freezer_table_test.go diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index 546db0c656..313ac8b78c 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -1,4 +1,4 @@ -// Copyright 2018 The go-ethereum Authors +// Copyright 2019 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "sync" + "sync/atomic" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -40,16 +41,47 @@ var ( errOutOfBounds = errors.New("out of bounds") ) +// indexEntry contains the number/id of the file that the data resides in, aswell as the +// offset within the file to the end of the data +// In serialized form, the filenum is stored as uint16. +type indexEntry struct { + filenum uint32 // stored as uint16 ( 2 bytes) + offset uint32 // stored as uint32 ( 4 bytes) +} + +const indexEntrySize = 6 + +// unmarshallBinary deserializes binary b into the rawIndex entry. +func (i *indexEntry) unmarshalBinary(b []byte) error { + i.filenum = uint32(binary.BigEndian.Uint16(b[:2])) + i.offset = binary.BigEndian.Uint32(b[2:6]) + return nil +} + +// marshallBinary serializes the rawIndex entry into binary. +func (i *indexEntry) marshallBinary() []byte { + b := make([]byte, indexEntrySize) + binary.BigEndian.PutUint16(b[:2], uint16(i.filenum)) + binary.BigEndian.PutUint32(b[2:6], i.offset) + return b +} + // freezerTable represents a single chained data table within the freezer (e.g. blocks). -// It consists of a data file (snappy encoded arbitrary data blobs) and an index +// It consists of a data file (snappy encoded arbitrary data blobs) and an indexEntry // file (uncompressed 64 bit indices into the data file). type freezerTable struct { - content *os.File // File descriptor for the data content of the table - offsets *os.File // File descriptor for the index file of the table + noCompression bool // if true, disables snappy compression. Note: does not work retroactively + maxFileSize uint32 // Max file size for data-files + name string + path string - items uint64 // Number of items stored in the table - bytes uint64 // Number of content bytes stored in the table + head *os.File // File descriptor for the data head of the table + files map[uint32]*os.File // open files + headId uint32 // number of the currently active head file + index *os.File // File descriptor for the indexEntry file of the table + items uint64 // Number of items stored in the table + headBytes uint32 // Number of bytes written to the head file readMeter metrics.Meter // Meter for measuring the effective amount of data read writeMeter metrics.Meter // Meter for measuring the effective amount of data written @@ -57,149 +89,231 @@ type freezerTable struct { lock sync.RWMutex // Mutex protecting the data file descriptors } -// newTable opens a freezer table, creating the data and index files if they are +// newTable opens a freezer table with default settings - 2G files and snappy compression +func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter) (*freezerTable, error) { + return newCustomTable(path, name, readMeter, writeMeter, 2*1000*1000*1000, false) +} + +// newCustomTable opens a freezer table, creating the data and index files if they are // non existent. Both files are truncated to the shortest common length to ensure // they don't go out of sync. -func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter) (*freezerTable, error) { - // Ensure the containing directory exists and open the two data files +func newCustomTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter, maxFilesize uint32, noCompression bool) (*freezerTable, error) { + // Ensure the containing directory exists and open the indexEntry file if err := os.MkdirAll(path, 0755); err != nil { return nil, err } - content, err := os.OpenFile(filepath.Join(path, name+".dat"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) - if err != nil { - return nil, err + var idxName string + if noCompression { + // raw idx + idxName = fmt.Sprintf("%s.ridx", name) + } else { + // compressed idx + idxName = fmt.Sprintf("%s.cidx", name) } - offsets, err := os.OpenFile(filepath.Join(path, name+".idx"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + offsets, err := os.OpenFile(filepath.Join(path, idxName), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { - content.Close() return nil, err } // Create the table and repair any past inconsistency tab := &freezerTable{ - content: content, - offsets: offsets, - readMeter: readMeter, - writeMeter: writeMeter, - logger: log.New("database", path, "table", name), + index: offsets, + files: make(map[uint32]*os.File), + readMeter: readMeter, + writeMeter: writeMeter, + name: name, + path: path, + logger: log.New("database", path, "table", name), + noCompression: noCompression, + maxFileSize: maxFilesize, } if err := tab.repair(); err != nil { - offsets.Close() - content.Close() + tab.Close() return nil, err } return tab, nil } -// repair cross checks the content and the offsets file and truncates them to +// repair cross checks the head and the index file and truncates them to // be in sync with each other after a potential crash / data loss. func (t *freezerTable) repair() error { - // Create a temporary offset buffer to init files with and read offsts into - offset := make([]byte, 8) + // Create a temporary offset buffer to init files with and read indexEntry into + buffer := make([]byte, indexEntrySize) - // If we've just created the files, initialize the offsets with the 0 index - stat, err := t.offsets.Stat() + // If we've just created the files, initialize the index with the 0 indexEntry + stat, err := t.index.Stat() if err != nil { return err } if stat.Size() == 0 { - if _, err := t.offsets.Write(offset); err != nil { + if _, err := t.index.Write(buffer); err != nil { return err } } - // Ensure the offsets are a multiple of 8 bytes - if overflow := stat.Size() % 8; overflow != 0 { - t.offsets.Truncate(stat.Size() - overflow) // New file can't trigger this path + // Ensure the index is a multiple of indexEntrySize bytes + if overflow := stat.Size() % indexEntrySize; overflow != 0 { + t.index.Truncate(stat.Size() - overflow) // New file can't trigger this path } // Retrieve the file sizes and prepare for truncation - if stat, err = t.offsets.Stat(); err != nil { + if stat, err = t.index.Stat(); err != nil { return err } offsetsSize := stat.Size() - if stat, err = t.content.Stat(); err != nil { + // Open the head file + var ( + lastIndex indexEntry + contentSize int64 + contentExp int64 + ) + t.index.ReadAt(buffer, offsetsSize-indexEntrySize) + lastIndex.unmarshalBinary(buffer) + t.head, err = t.openFile(lastIndex.filenum, os.O_RDWR|os.O_CREATE|os.O_APPEND) + if err != nil { return err } - contentSize := stat.Size() + if stat, err = t.head.Stat(); err != nil { + return err + } + contentSize = stat.Size() // Keep truncating both files until they come in sync - t.offsets.ReadAt(offset, offsetsSize-8) - contentExp := int64(binary.LittleEndian.Uint64(offset)) + contentExp = int64(lastIndex.offset) for contentExp != contentSize { - // Truncate the content file to the last offset pointer + // Truncate the head file to the last offset pointer if contentExp < contentSize { - t.logger.Warn("Truncating dangling content", "indexed", common.StorageSize(contentExp), "stored", common.StorageSize(contentSize)) - if err := t.content.Truncate(contentExp); err != nil { + t.logger.Warn("Truncating dangling head", "indexed", common.StorageSize(contentExp), "stored", common.StorageSize(contentSize)) + if err := t.head.Truncate(contentExp); err != nil { return err } contentSize = contentExp } - // Truncate the offsets to point within the content file + // Truncate the index to point within the head file if contentExp > contentSize { - t.logger.Warn("Truncating dangling offsets", "indexed", common.StorageSize(contentExp), "stored", common.StorageSize(contentSize)) - if err := t.offsets.Truncate(offsetsSize - 8); err != nil { + t.logger.Warn("Truncating dangling indexes", "indexed", common.StorageSize(contentExp), "stored", common.StorageSize(contentSize)) + if err := t.index.Truncate(offsetsSize - indexEntrySize); err != nil { return err } - offsetsSize -= 8 - - t.offsets.ReadAt(offset, offsetsSize-8) - contentExp = int64(binary.LittleEndian.Uint64(offset)) + offsetsSize -= indexEntrySize + t.index.ReadAt(buffer, offsetsSize-indexEntrySize) + var newLastIndex indexEntry + newLastIndex.unmarshalBinary(buffer) + // We might have slipped back into an earlier head-file here + if newLastIndex.filenum != lastIndex.filenum { + // release earlier opened file + t.releaseFile(lastIndex.filenum) + t.head, err = t.openFile(newLastIndex.filenum, os.O_RDWR|os.O_CREATE|os.O_APPEND) + if stat, err = t.head.Stat(); err != nil { + // TODO, anything more we can do here? + // A data file has gone missing... + return err + } + contentSize = stat.Size() + } + lastIndex = newLastIndex + contentExp = int64(lastIndex.offset) } } // Ensure all reparation changes have been written to disk - if err := t.offsets.Sync(); err != nil { + if err := t.index.Sync(); err != nil { return err } - if err := t.content.Sync(); err != nil { + if err := t.head.Sync(); err != nil { return err } // Update the item and byte counters and return - t.items = uint64(offsetsSize/8 - 1) // last index points to the end of the data file - t.bytes = uint64(contentSize) + t.items = uint64(offsetsSize/indexEntrySize - 1) // last indexEntry points to the end of the data file + t.headBytes = uint32(contentSize) + t.headId = lastIndex.filenum - t.logger.Debug("Chain freezer table opened", "items", t.items, "size", common.StorageSize(t.bytes)) + // Close opened files and preopen all files + if err := t.preopen(); err != nil { + return err + } + t.logger.Debug("Chain freezer table opened", "items", t.items, "size", common.StorageSize(t.headBytes)) return nil } +// preopen opens all files that the freezer will need. This method should be called from an init-context, +// since it assumes that it doesn't have to bother with locking +// The rationale for doing preopen is to not have to do it from within Retrieve, thus not needing to ever +// obtain a write-lock within Retrieve. +func (t *freezerTable) preopen() (err error) { + // The repair might have already opened (some) files + t.releaseFilesAfter(0, false) + // Open all except head in RDONLY + for i := uint32(0); i < t.headId; i++ { + if _, err = t.openFile(i, os.O_RDONLY); err != nil { + return err + } + } + // Open head in read/write + t.head, err = t.openFile(t.headId, os.O_RDWR|os.O_CREATE|os.O_APPEND) + return err +} + // truncate discards any recent data above the provided threashold number. func (t *freezerTable) truncate(items uint64) error { + t.lock.Lock() + defer t.lock.Unlock() // If out item count is corrent, don't do anything - if t.items <= items { + if atomic.LoadUint64(&t.items) <= items { return nil } // Something's out of sync, truncate the table's offset index t.logger.Warn("Truncating freezer table", "items", t.items, "limit", items) - if err := t.offsets.Truncate(int64(items+1) * 8); err != nil { + if err := t.index.Truncate(int64(items+1) * indexEntrySize); err != nil { return err } // Calculate the new expected size of the data file and truncate it - offset := make([]byte, 8) - t.offsets.ReadAt(offset, int64(items)*8) - expected := binary.LittleEndian.Uint64(offset) + buffer := make([]byte, indexEntrySize) + if _, err := t.index.ReadAt(buffer, int64(items*indexEntrySize)); err != nil { + return err + } + var expected indexEntry + expected.unmarshalBinary(buffer) + // We might need to truncate back to older files + if expected.filenum != t.headId { + // If already open for reading, force-reopen for writing + t.releaseFile(expected.filenum) + newHead, err := t.openFile(expected.filenum, os.O_RDWR|os.O_CREATE|os.O_APPEND) + if err != nil { + return err + } + // release any files _after the current head -- both the previous head + // and any files which may have been opened for reading + t.releaseFilesAfter(expected.filenum, true) + // set back the historic head + t.head = newHead + atomic.StoreUint32(&t.headId, expected.filenum) + } - if err := t.content.Truncate(int64(expected)); err != nil { + if err := t.head.Truncate(int64(expected.offset)); err != nil { return err } // All data files truncated, set internal counters and return - t.items, t.bytes = items, expected + atomic.StoreUint64(&t.items, items) + atomic.StoreUint32(&t.headBytes, expected.offset) return nil } -// Close unmaps all active memory mapped regions. +// Close closes all opened files. func (t *freezerTable) Close() error { t.lock.Lock() defer t.lock.Unlock() var errs []error - if err := t.offsets.Close(); err != nil { + if err := t.index.Close(); err != nil { errs = append(errs, err) } - t.offsets = nil + t.index = nil - if err := t.content.Close(); err != nil { - errs = append(errs, err) + for _, f := range t.files { + if err := f.Close(); err != nil { + errs = append(errs, err) + } } - t.content = nil + t.head = nil if errs != nil { return fmt.Errorf("%v", errs) @@ -207,78 +321,173 @@ func (t *freezerTable) Close() error { return nil } -// Append injects a binary blob at the end of the freezer table. The item index +// openFile assumes that the write-lock is held by the caller +func (t *freezerTable) openFile(num uint32, flag int) (f *os.File, err error) { + var exist bool + if f, exist = t.files[num]; !exist { + var name string + if t.noCompression { + name = fmt.Sprintf("%s.%d.rdat", t.name, num) + } else { + name = fmt.Sprintf("%s.%d.cdat", t.name, num) + } + f, err = os.OpenFile(filepath.Join(t.path, name), flag, 0644) + if err != nil { + return nil, err + } + t.files[num] = f + } + return f, err +} + +// releaseFile closes a file, and removes it from the open file cache. +// Assumes that the caller holds the write lock +func (t *freezerTable) releaseFile(num uint32) { + if f, exist := t.files[num]; exist { + delete(t.files, num) + f.Close() + } +} + +// releaseFilesAfter closes all open files with a higher number, and optionally also deletes the files +func (t *freezerTable) releaseFilesAfter(num uint32, remove bool) { + for fnum, f := range t.files { + if fnum > num { + delete(t.files, fnum) + f.Close() + if remove { + os.Remove(f.Name()) + } + } + } +} + +// Append injects a binary blob at the end of the freezer table. The item number // is a precautionary parameter to ensure data correctness, but the table will // reject already existing data. // // Note, this method will *not* flush any data to disk so be sure to explicitly // fsync before irreversibly deleting data from the database. func (t *freezerTable) Append(item uint64, blob []byte) error { + // Read lock prevents competition with truncate + t.lock.RLock() // Ensure the table is still accessible - if t.offsets == nil || t.content == nil { + if t.index == nil || t.head == nil { return errClosed } // Ensure only the next item can be written, nothing else - if t.items != item { + if atomic.LoadUint64(&t.items) != item { panic(fmt.Sprintf("appending unexpected item: want %d, have %d", t.items, item)) } // Encode the blob and write it into the data file - blob = snappy.Encode(nil, blob) - if _, err := t.content.Write(blob); err != nil { + if !t.noCompression { + blob = snappy.Encode(nil, blob) + } + bLen := uint32(len(blob)) + if t.headBytes+bLen < bLen || + t.headBytes+bLen > t.maxFileSize { + // we need a new file, writing would overflow + t.lock.RUnlock() + t.lock.Lock() + nextId := atomic.LoadUint32(&t.headId) + 1 + // We open the next file in truncated mode -- if this file already + // exists, we need to start over from scratch on it + newHead, err := t.openFile(nextId, os.O_RDWR|os.O_CREATE|os.O_TRUNC) + if err != nil { + t.lock.Unlock() + return err + } + // Close old file, and reopen in RDONLY mode + t.releaseFile(t.headId) + t.openFile(t.headId, os.O_RDONLY) + + // Swap out the current head + t.head = newHead + atomic.StoreUint32(&t.headBytes, 0) + atomic.StoreUint32(&t.headId, nextId) + t.lock.Unlock() + t.lock.RLock() + } + + defer t.lock.RUnlock() + + if _, err := t.head.Write(blob); err != nil { return err } - t.bytes += uint64(len(blob)) - - offset := make([]byte, 8) - binary.LittleEndian.PutUint64(offset, t.bytes) - if _, err := t.offsets.Write(offset); err != nil { - return err + newOffset := atomic.AddUint32(&t.headBytes, bLen) + idx := indexEntry{ + filenum: atomic.LoadUint32(&t.headId), + offset: newOffset, } - t.items++ - - t.writeMeter.Mark(int64(len(blob) + 8)) // 8 = 1 x 8 byte offset + // Write indexEntry + t.index.Write(idx.marshallBinary()) + t.writeMeter.Mark(int64(bLen + indexEntrySize)) + atomic.AddUint64(&t.items, 1) return nil } -// Retrieve looks up the data offset of an item with the given index and retrieves +// getBounds returns the indexes for the item +// returns start, end, filenumber and error +func (t *freezerTable) getBounds(item uint64) (uint32, uint32, uint32, error) { + var startIdx, endIdx indexEntry + buffer := make([]byte, indexEntrySize) + if _, err := t.index.ReadAt(buffer, int64(item*indexEntrySize)); err != nil { + return 0, 0, 0, err + } + startIdx.unmarshalBinary(buffer) + if _, err := t.index.ReadAt(buffer, int64((item+1)*indexEntrySize)); err != nil { + return 0, 0, 0, err + } + endIdx.unmarshalBinary(buffer) + if startIdx.filenum != endIdx.filenum { + // If a piece of data 'crosses' a data-file, + // it's actually in one piece on the second data-file. + // We return a zero-indexEntry for the second file as start + return 0, endIdx.offset, endIdx.filenum, nil + } + return startIdx.offset, endIdx.offset, endIdx.filenum, nil +} + +// Retrieve looks up the data offset of an item with the given number and retrieves // the raw binary blob from the data file. func (t *freezerTable) Retrieve(item uint64) ([]byte, error) { - t.lock.RLock() - defer t.lock.RUnlock() // Ensure the table and the item is accessible - if t.offsets == nil || t.content == nil { + if t.index == nil || t.head == nil { return nil, errClosed } - if t.items <= item { + if atomic.LoadUint64(&t.items) <= item { return nil, errOutOfBounds } - // Item reachable, retrieve the data content boundaries - offset := make([]byte, 8) - if _, err := t.offsets.ReadAt(offset, int64(item*8)); err != nil { + t.lock.RLock() + startOffset, endOffset, filenum, err := t.getBounds(item) + if err != nil { return nil, err } - start := binary.LittleEndian.Uint64(offset) - - if _, err := t.offsets.ReadAt(offset, int64((item+1)*8)); err != nil { - return nil, err + dataFile, exist := t.files[filenum] + if !exist { + return nil, fmt.Errorf("missing data file %d", filenum) } - end := binary.LittleEndian.Uint64(offset) - // Retrieve the data itself, decompress and return - blob := make([]byte, end-start) - if _, err := t.content.ReadAt(blob, int64(start)); err != nil { + blob := make([]byte, endOffset-startOffset) + if _, err := dataFile.ReadAt(blob, int64(startOffset)); err != nil { + t.lock.RUnlock() return nil, err } - t.readMeter.Mark(int64(len(blob) + 16)) // 16 = 2 x 8 byte offset + t.lock.RUnlock() + t.readMeter.Mark(int64(len(blob) + 2*indexEntrySize)) + + if t.noCompression { + return blob, nil + } return snappy.Decode(nil, blob) } // Sync pushes any pending data from memory out to disk. This is an expensive // operation, so use it with care. func (t *freezerTable) Sync() error { - if err := t.offsets.Sync(); err != nil { + if err := t.index.Sync(); err != nil { return err } - return t.content.Sync() + return t.head.Sync() } diff --git a/core/rawdb/freezer_table_test.go b/core/rawdb/freezer_table_test.go new file mode 100644 index 0000000000..d6ce6e93c0 --- /dev/null +++ b/core/rawdb/freezer_table_test.go @@ -0,0 +1,511 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/metrics" + "math/rand" + "os" + "path/filepath" + "testing" + "time" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +// Gets a chunk of data, filled with 'b' +func getChunk(size int, b byte) []byte { + data := make([]byte, size) + for i, _ := range data { + data[i] = b + } + return data +} + +func print(t *testing.T, f *freezerTable, item uint64) { + a, err := f.Retrieve(item) + if err != nil { + t.Fatal(err) + } + fmt.Printf("db[%d] = %x\n", item, a) +} + +// TestFreezerBasics test initializing a freezertable from scratch, writing to the table, +// and reading it back. +func TestFreezerBasics(t *testing.T) { + t.Parallel() + // set cutoff at 50 bytes + f, err := newCustomTable(os.TempDir(), + fmt.Sprintf("unittest-%d", rand.Uint64()), + metrics.NewMeter(), metrics.NewMeter(), 50, true) + if err != nil { + t.Fatal(err) + } + defer f.Close() + // Write 15 bytes 255 times, results in 85 files + for x := byte(0); x < 255; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + + //print(t, f, 0) + //print(t, f, 1) + //print(t, f, 2) + // + //db[0] = 000000000000000000000000000000 + //db[1] = 010101010101010101010101010101 + //db[2] = 020202020202020202020202020202 + + for y := byte(0); y < 255; y++ { + exp := getChunk(15, y) + got, err := f.Retrieve(uint64(y)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, exp) { + t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) + } + } +} + +// TestFreezerBasicsClosing tests same as TestFreezerBasics, but also closes and reopens the freezer between +// every operation +func TestFreezerBasicsClosing(t *testing.T) { + t.Parallel() + // set cutoff at 50 bytes + var ( + fname = fmt.Sprintf("basics-close-%d", rand.Uint64()) + m1, m2 = metrics.NewMeter(), metrics.NewMeter() + f *freezerTable + err error + ) + f, err = newCustomTable(os.TempDir(), fname, m1, m2, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times, results in 85 files + for x := byte(0); x < 255; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + f.Close() + f, err = newCustomTable(os.TempDir(), fname, m1, m2, 50, true) + if err != nil { + t.Fatal(err) + } + } + defer f.Close() + + for y := byte(0); y < 255; y++ { + exp := getChunk(15, y) + got, err := f.Retrieve(uint64(y)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, exp) { + t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) + } + f.Close() + f, err = newCustomTable(os.TempDir(), fname, m1, m2, 50, true) + if err != nil { + t.Fatal(err) + } + } +} + +// TestFreezerRepairDanglingHead tests that we can recover if index entries are removed +func TestFreezerRepairDanglingHead(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64()) + + { // Fill table + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times + for x := byte(0); x < 0xff; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + // The last item should be there + if _, err = f.Retrieve(0xfe); err != nil { + t.Fatal(err) + } + f.Close() + } + // open the index + idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644) + if err != nil { + t.Fatalf("Failed to open index file: %v", err) + } + // Remove 4 bytes + stat, err := idxFile.Stat() + if err != nil { + t.Fatalf("Failed to stat index file: %v", err) + } + idxFile.Truncate(stat.Size() - 4) + idxFile.Close() + // Now open it again + { + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + // The last item should be missing + if _, err = f.Retrieve(0xff); err == nil { + t.Errorf("Expected error for missing index entry") + } + // The one before should still be there + if _, err = f.Retrieve(0xfd); err != nil { + t.Fatalf("Expected no error, got %v", err) + } + } +} + +// TestFreezerRepairDanglingHeadLarge tests that we can recover if very many index entries are removed +func TestFreezerRepairDanglingHeadLarge(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64()) + + { // Fill a table and close it + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times + for x := byte(0); x < 0xff; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + // The last item should be there + if _, err = f.Retrieve(f.items - 1); err == nil { + if err != nil { + t.Fatal(err) + } + } + f.Close() + } + // open the index + idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644) + if err != nil { + t.Fatalf("Failed to open index file: %v", err) + } + // Remove everything but the first item, and leave data unaligned + // 0-indexEntry, 1-indexEntry, corrupt-indexEntry + idxFile.Truncate(indexEntrySize + indexEntrySize + indexEntrySize/2) + idxFile.Close() + // Now open it again + { + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + // The first item should be there + if _, err = f.Retrieve(0); err != nil { + t.Fatal(err) + } + // The second item should be missing + if _, err = f.Retrieve(1); err == nil { + t.Errorf("Expected error for missing index entry") + } + // We should now be able to store items again, from item = 1 + for x := byte(1); x < 0xff; x++ { + data := getChunk(15, ^x) + f.Append(uint64(x), data) + } + f.Close() + } + // And if we open it, we should now be able to read all of them (new values) + { + f, _ := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + for y := byte(1); y < 255; y++ { + exp := getChunk(15, ^y) + got, err := f.Retrieve(uint64(y)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, exp) { + t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) + } + } + } +} + +// TestSnappyDetection tests that we fail to open a snappy database and vice versa +func TestSnappyDetection(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("snappytest-%d", rand.Uint64()) + // Open with snappy + { + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times + for x := byte(0); x < 0xff; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + f.Close() + } + // Open without snappy + { + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, false) + if _, err = f.Retrieve(0); err == nil { + f.Close() + t.Fatalf("expected empty table") + } + } + + // Open with snappy + { + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + // There should be 255 items + if _, err = f.Retrieve(0xfe); err != nil { + f.Close() + t.Fatalf("expected no error, got %v", err) + } + } + +} +func assertFileSize(f string, size int64) error { + stat, err := os.Stat(f) + if err != nil { + return err + } + if stat.Size() != size { + return fmt.Errorf("error, expected size %d, got %d", size, stat.Size()) + } + return nil + +} + +// TestFreezerRepairDanglingIndex checks that if the index has more entries than there are data, +// the index is repaired +func TestFreezerRepairDanglingIndex(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("dangling_indextest-%d", rand.Uint64()) + + { // Fill a table and close it + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 9 times : 150 bytes + for x := byte(0); x < 9; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + // The last item should be there + if _, err = f.Retrieve(f.items - 1); err != nil { + f.Close() + t.Fatal(err) + } + f.Close() + // File sizes should be 45, 45, 45 : items[3, 3, 3) + } + // Crop third file + fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.2.rdat", fname)) + // Truncate third file: 45 ,45, 20 + { + if err := assertFileSize(fileToCrop, 45); err != nil { + t.Fatal(err) + } + file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + file.Truncate(20) + file.Close() + } + // Open db it again + // It should restore the file(s) to + // 45, 45, 15 + // with 3+3+1 items + { + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + if err != nil { + t.Fatal(err) + } + if f.items != 7 { + f.Close() + t.Fatalf("expected %d items, got %d", 7, f.items) + } + if err := assertFileSize(fileToCrop, 15); err != nil { + t.Fatal(err) + } + } +} + +func TestFreezerTruncate(t *testing.T) { + + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("truncation-%d", rand.Uint64()) + + { // Fill table + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 30 times + for x := byte(0); x < 30; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + // The last item should be there + if _, err = f.Retrieve(f.items - 1); err != nil { + t.Fatal(err) + } + f.Close() + } + // Reopen, truncate + { + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + if err != nil { + t.Fatal(err) + } + defer f.Close() + f.truncate(10) // 150 bytes + if f.items != 10 { + t.Fatalf("expected %d items, got %d", 10, f.items) + } + // 45, 45, 45, 15 -- bytes should be 15 + if f.headBytes != 15 { + t.Fatalf("expected %d bytes, got %d", 15, f.headBytes) + } + + } + +} + +// TestFreezerRepairFirstFile tests a head file with the very first item only half-written. +// That will rewind the index, and _should_ truncate the head file +func TestFreezerRepairFirstFile(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("truncationfirst-%d", rand.Uint64()) + { // Fill table + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 80 bytes, splitting out into two files + f.Append(0, getChunk(40, 0xFF)) + f.Append(1, getChunk(40, 0xEE)) + // The last item should be there + if _, err = f.Retrieve(f.items - 1); err != nil { + t.Fatal(err) + } + f.Close() + } + // Truncate the file in half + fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.1.rdat", fname)) + { + if err := assertFileSize(fileToCrop, 40); err != nil { + t.Fatal(err) + } + file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + file.Truncate(20) + file.Close() + } + // Reopen + { + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + if err != nil { + t.Fatal(err) + } + if f.items != 1 { + f.Close() + t.Fatalf("expected %d items, got %d", 0, f.items) + } + // Write 40 bytes + f.Append(1, getChunk(40, 0xDD)) + f.Close() + // Should have been truncated down to zero and then 40 written + if err := assertFileSize(fileToCrop, 40); err != nil { + t.Fatal(err) + } + } +} + +// TestFreezerReadAndTruncate tests: +// - we have a table open +// - do some reads, so files are open in readonly +// - truncate so those files are 'removed' +// - check that we did not keep the rdonly file descriptors +func TestFreezerReadAndTruncate(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("read_truncate-%d", rand.Uint64()) + { // Fill table + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 30 times + for x := byte(0); x < 30; x++ { + data := getChunk(15, x) + f.Append(uint64(x), data) + } + // The last item should be there + if _, err = f.Retrieve(f.items - 1); err != nil { + t.Fatal(err) + } + f.Close() + } + // Reopen and read all files + { + f, err := newCustomTable(os.TempDir(), fname, wm, rm, 50, true) + if err != nil { + t.Fatal(err) + } + if f.items != 30 { + f.Close() + t.Fatalf("expected %d items, got %d", 0, f.items) + } + for y := byte(0); y < 30; y++ { + f.Retrieve(uint64(y)) + } + // Now, truncate back to zero + f.truncate(0) + // Write the data again + for x := byte(0); x < 30; x++ { + data := getChunk(15, ^x) + if err := f.Append(uint64(x), data); err != nil { + t.Fatalf("error %v", err) + } + } + f.Close() + } +} + +// TODO (?) +// - test that if we remove several head-files, aswell as data last data-file, +// the index is truncated accordingly +// Right now, the freezer would fail on these conditions: +// 1. have data files d0, d1, d2, d3 +// 2. remove d2,d3 +// +// However, all 'normal' failure modes arising due to failing to sync() or save a file should be +// handled already, and the case described above can only (?) happen if an external process/user +// deletes files from the filesystem. From b6cac42e9ffc0b19a1e70416db85593f1cb0d30c Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Thu, 14 Mar 2019 14:59:47 +0800 Subject: [PATCH 20/34] core/rawdb: add file lock for freezer --- core/rawdb/database.go | 22 ++++++++++++++++++++-- core/rawdb/freezer.go | 19 ++++++++++++++++--- ethdb/database.go | 7 +++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 0f994c3fd3..cd1048cbc5 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -17,6 +17,8 @@ package rawdb import ( + "fmt" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/leveldb" "github.com/ethereum/go-ethereum/ethdb/memorydb" @@ -25,7 +27,23 @@ import ( // freezerdb is a databse wrapper that enabled freezer data retrievals. type freezerdb struct { ethdb.KeyValueStore - ethdb.Ancienter + ethdb.AncientStore +} + +// Close implements io.Closer, closing both the fast key-value store as well as +// the slow ancient tables. +func (frdb *freezerdb) Close() error { + var errs []error + if err := frdb.KeyValueStore.Close(); err != nil { + errs = append(errs, err) + } + if err := frdb.AncientStore.Close(); err != nil { + errs = append(errs, err) + } + if len(errs) != 0 { + return fmt.Errorf("%v", errs) + } + return nil } // nofreezedb is a database wrapper that disables freezer data retrievals. @@ -58,7 +76,7 @@ func NewDatabaseWithFreezer(db ethdb.KeyValueStore, freezer string, namespace st return &freezerdb{ KeyValueStore: db, - Ancienter: frdb, + AncientStore: frdb, }, nil } diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 4f227e3b78..07df4c7599 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "math" + "path/filepath" "sync/atomic" "time" @@ -27,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/prometheus/tsdb/fileutil" ) // errUnknownTable is returned if the user attempts to read from a table that is @@ -57,8 +59,9 @@ const ( // reserving it for go-ethereum. This would also reduce the memory requirements // of Geth, and thus also GC overhead. type freezer struct { - tables map[string]*freezerTable // Data tables for storing everything - frozen uint64 // Number of blocks already frozen + tables map[string]*freezerTable // Data tables for storing everything + frozen uint64 // Number of blocks already frozen + instanceLock fileutil.Releaser // File-system lock to prevent double opens } // newFreezer creates a chain freezer that moves ancient chain data into @@ -69,9 +72,14 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) ) + lock, _, err := fileutil.Flock(filepath.Join(datadir, "LOCK")) + if err != nil { + return nil, err + } // Open all the supported data tables freezer := &freezer{ - tables: make(map[string]*freezerTable), + tables: make(map[string]*freezerTable), + instanceLock: lock, } for _, name := range []string{"hashes", "headers", "bodies", "receipts", "diffs"} { table, err := newTable(datadir, name, readMeter, writeMeter) @@ -79,6 +87,7 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { for _, table := range freezer.tables { table.Close() } + lock.Release() return nil, err } freezer.tables[name] = table @@ -95,6 +104,7 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { for _, table := range freezer.tables { table.Close() } + lock.Release() return nil, err } } @@ -109,6 +119,9 @@ func (f *freezer) Close() error { errs = append(errs, err) } } + if err := f.instanceLock.Release(); err != nil { + errs = append(errs, err) + } if errs != nil { return fmt.Errorf("%v", errs) } diff --git a/ethdb/database.go b/ethdb/database.go index 764e304e3c..01483f3d4d 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -80,6 +80,13 @@ type AncientReader interface { Ancienter } +// AncientStore contains all the methods required to allow handling different +// ancient data stores backing immutable chain data store. +type AncientStore interface { + Ancienter + io.Closer +} + // Database contains all the methods required by the high level database to not // only access the key-value data store but also the chain freezer. type Database interface { From 80469bea0cc6dbfae749d944094a7c2357dc050d Mon Sep 17 00:00:00 2001 From: gary rong Date: Thu, 25 Apr 2019 22:59:48 +0800 Subject: [PATCH 21/34] all: integrate the freezer with fast sync * all: freezer style syncing core, eth, les, light: clean up freezer relative APIs core, eth, les, trie, ethdb, light: clean a bit core, eth, les, light: add unit tests core, light: rewrite setHead function core, eth: fix downloader unit tests core: add receipt chain insertion test core: use constant instead of hardcoding table name core: fix rollback core: fix setHead core/rawdb: remove canonical block first and then iterate side chain core/rawdb, ethdb: add hasAncient interface eth/downloader: calculate ancient limit via cht first core, eth, ethdb: lots of fixes * eth/downloader: print ancient disable log only for fast sync --- core/blockchain.go | 395 +++++++++++++++++++++++------- core/blockchain_test.go | 211 ++++++++++++++-- core/headerchain.go | 64 +++-- core/rawdb/accessors_chain.go | 178 ++++++++++---- core/rawdb/accessors_indexes.go | 12 +- core/rawdb/accessors_metadata.go | 12 +- core/rawdb/database.go | 31 ++- core/rawdb/freezer.go | 212 +++++++++++----- core/rawdb/freezer_table.go | 10 +- core/rawdb/schema.go | 17 ++ core/rawdb/table.go | 32 ++- core/state/database.go | 2 +- core/state/sync.go | 2 +- eth/downloader/downloader.go | 54 +++- eth/downloader/downloader_test.go | 72 +++++- ethdb/batch.go | 4 +- ethdb/database.go | 56 +++-- ethdb/leveldb/leveldb.go | 4 +- ethdb/memorydb/memorydb.go | 2 +- les/odr_requests.go | 2 +- light/lightchain.go | 6 +- light/nodeset.go | 6 +- light/trie.go | 2 +- trie/database.go | 2 +- trie/proof.go | 8 +- trie/sync.go | 6 +- 26 files changed, 1076 insertions(+), 326 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 61b809319c..4ac2c3a44e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -63,6 +63,8 @@ var ( blockPrefetchExecuteTimer = metrics.NewRegisteredTimer("chain/prefetch/executes", nil) blockPrefetchInterruptMeter = metrics.NewRegisteredMeter("chain/prefetch/interrupts", nil) + + errInsertionInterrupted = errors.New("insertion is interrupted") ) const ( @@ -138,7 +140,6 @@ type BlockChain struct { chainmu sync.RWMutex // blockchain insertion lock - checkpoint int // checkpoint counts towards the new checkpoint currentBlock atomic.Value // Current head of the block chain currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!) @@ -161,8 +162,9 @@ type BlockChain struct { processor Processor // Block transaction processor interface vmConfig vm.Config - badBlocks *lru.Cache // Bad block cache - shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. + badBlocks *lru.Cache // Bad block cache + shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. + terminateInsert func(common.Hash, uint64) bool // Testing hook used to terminate ancient receipt chain insertion. } // NewBlockChain returns a fully initialised block chain using information @@ -216,6 +218,39 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par if err := bc.loadLastState(); err != nil { return nil, err } + if frozen, err := bc.db.Ancients(); err == nil && frozen >= 1 { + var ( + needRewind bool + low uint64 + ) + // The head full block may be rolled back to a very low height due to + // blockchain repair. If the head full block is even lower than the ancient + // chain, truncate the ancient store. + fullBlock := bc.CurrentBlock() + if fullBlock != nil && fullBlock != bc.genesisBlock && fullBlock.NumberU64() < frozen-1 { + needRewind = true + low = fullBlock.NumberU64() + } + // In fast sync, it may happen that ancient data has been written to the + // ancient store, but the LastFastBlock has not been updated, truncate the + // extra data here. + fastBlock := bc.CurrentFastBlock() + if fastBlock != nil && fastBlock.NumberU64() < frozen-1 { + needRewind = true + if fastBlock.NumberU64() < low || low == 0 { + low = fastBlock.NumberU64() + } + } + if needRewind { + var hashes []common.Hash + previous := bc.CurrentHeader().Number.Uint64() + for i := low + 1; i <= bc.CurrentHeader().Number.Uint64(); i++ { + hashes = append(hashes, rawdb.ReadCanonicalHash(bc.db, i)) + } + bc.Rollback(hashes) + log.Warn("Truncate ancient chain", "from", previous, "to", low) + } + } // Check the current state of the block hashes and make sure that we do not have any of the bad blocks in our chain for hash := range BadHashes { if header := bc.GetHeaderByHash(hash); header != nil { @@ -267,6 +302,7 @@ func (bc *BlockChain) loadLastState() error { if err := bc.repair(¤tBlock); err != nil { return err } + rawdb.WriteHeadBlockHash(bc.db, currentBlock.Hash()) } // Everything seems to be fine, set as the head block bc.currentBlock.Store(currentBlock) @@ -312,12 +348,55 @@ func (bc *BlockChain) SetHead(head uint64) error { bc.chainmu.Lock() defer bc.chainmu.Unlock() - // Rewind the header chain, deleting all block bodies until then - delFn := func(db ethdb.Writer, hash common.Hash, num uint64) { - rawdb.DeleteBody(db, hash, num) + updateFn := func(db ethdb.KeyValueWriter, header *types.Header) { + // Rewind the block chain, ensuring we don't end up with a stateless head block + if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() < currentBlock.NumberU64() { + newHeadBlock := bc.GetBlock(header.Hash(), header.Number.Uint64()) + if newHeadBlock == nil { + newHeadBlock = bc.genesisBlock + } else { + if _, err := state.New(newHeadBlock.Root(), bc.stateCache); err != nil { + // Rewound state missing, rolled back to before pivot, reset to genesis + newHeadBlock = bc.genesisBlock + } + } + rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash()) + bc.currentBlock.Store(newHeadBlock) + } + + // Rewind the fast block in a simpleton way to the target head + if currentFastBlock := bc.CurrentFastBlock(); currentFastBlock != nil && header.Number.Uint64() < currentFastBlock.NumberU64() { + newHeadFastBlock := bc.GetBlock(header.Hash(), header.Number.Uint64()) + // If either blocks reached nil, reset to the genesis state + if newHeadFastBlock == nil { + newHeadFastBlock = bc.genesisBlock + } + rawdb.WriteHeadFastBlockHash(db, newHeadFastBlock.Hash()) + bc.currentFastBlock.Store(newHeadFastBlock) + } } - bc.hc.SetHead(head, delFn) - currentHeader := bc.hc.CurrentHeader() + + // Rewind the header chain, deleting all block bodies until then + delFn := func(db ethdb.KeyValueWriter, hash common.Hash, num uint64) { + // Ignore the error here since light client won't hit this path + frozen, _ := bc.db.Ancients() + if num+1 <= frozen { + // Truncate all relative data(header, total difficulty, body, receipt + // and canonical hash) from ancient store. + bc.db.TruncateAncients(num + 1) + + // Remove the hash <-> number mapping from the active store. + rawdb.DeleteHeaderNumber(db, hash) + } else { + // Remove relative body and receipts from the active store. + // The header, total difficulty and canonical hash will be + // removed in the hc.SetHead function. + rawdb.DeleteBody(db, hash, num) + rawdb.DeleteReceipts(db, hash, num) + } + // Todo(rjl493456442) txlookup, bloombits, etc + } + bc.hc.SetHead(head, updateFn, delFn) // Clear out any stale content from the caches bc.bodyCache.Purge() @@ -326,33 +405,6 @@ func (bc *BlockChain) SetHead(head uint64) error { bc.blockCache.Purge() bc.futureBlocks.Purge() - // Rewind the block chain, ensuring we don't end up with a stateless head block - if currentBlock := bc.CurrentBlock(); currentBlock != nil && currentHeader.Number.Uint64() < currentBlock.NumberU64() { - bc.currentBlock.Store(bc.GetBlock(currentHeader.Hash(), currentHeader.Number.Uint64())) - } - if currentBlock := bc.CurrentBlock(); currentBlock != nil { - if _, err := state.New(currentBlock.Root(), bc.stateCache); err != nil { - // Rewound state missing, rolled back to before pivot, reset to genesis - bc.currentBlock.Store(bc.genesisBlock) - } - } - // Rewind the fast block in a simpleton way to the target head - if currentFastBlock := bc.CurrentFastBlock(); currentFastBlock != nil && currentHeader.Number.Uint64() < currentFastBlock.NumberU64() { - bc.currentFastBlock.Store(bc.GetBlock(currentHeader.Hash(), currentHeader.Number.Uint64())) - } - // If either blocks reached nil, reset to the genesis state - if currentBlock := bc.CurrentBlock(); currentBlock == nil { - bc.currentBlock.Store(bc.genesisBlock) - } - if currentFastBlock := bc.CurrentFastBlock(); currentFastBlock == nil { - bc.currentFastBlock.Store(bc.genesisBlock) - } - currentBlock := bc.CurrentBlock() - currentFastBlock := bc.CurrentFastBlock() - - rawdb.WriteHeadBlockHash(bc.db, currentBlock.Hash()) - rawdb.WriteHeadFastBlockHash(bc.db, currentFastBlock.Hash()) - return bc.loadLastState() } @@ -780,96 +832,259 @@ func (bc *BlockChain) Rollback(chain []common.Hash) { } if currentFastBlock := bc.CurrentFastBlock(); currentFastBlock.Hash() == hash { newFastBlock := bc.GetBlock(currentFastBlock.ParentHash(), currentFastBlock.NumberU64()-1) - bc.currentFastBlock.Store(newFastBlock) rawdb.WriteHeadFastBlockHash(bc.db, newFastBlock.Hash()) + bc.currentFastBlock.Store(newFastBlock) } if currentBlock := bc.CurrentBlock(); currentBlock.Hash() == hash { newBlock := bc.GetBlock(currentBlock.ParentHash(), currentBlock.NumberU64()-1) - bc.currentBlock.Store(newBlock) rawdb.WriteHeadBlockHash(bc.db, newBlock.Hash()) + bc.currentBlock.Store(newBlock) } } + // Truncate ancient data which exceeds the current header. + // + // Notably, it can happen that system crashes without truncating the ancient data + // but the head indicator has been updated in the active store. Regarding this issue, + // system will self recovery by truncating the extra data during the setup phase. + if err := bc.truncateAncient(bc.hc.CurrentHeader().Number.Uint64()); err != nil { + log.Crit("Truncate ancient store failed", "err", err) + } +} + +// truncateAncient rewinds the blockchain to the specified header and deletes all +// data in the ancient store that exceeds the specified header. +func (bc *BlockChain) truncateAncient(head uint64) error { + frozen, err := bc.db.Ancients() + if err != nil { + return err + } + // Short circuit if there is no data to truncate in ancient store. + if frozen <= head+1 { + return nil + } + // Truncate all the data in the freezer beyond the specified head + if err := bc.db.TruncateAncients(head + 1); err != nil { + return err + } + // Clear out any stale content from the caches + bc.hc.headerCache.Purge() + bc.hc.tdCache.Purge() + bc.hc.numberCache.Purge() + + // Clear out any stale content from the caches + bc.bodyCache.Purge() + bc.bodyRLPCache.Purge() + bc.receiptsCache.Purge() + bc.blockCache.Purge() + bc.futureBlocks.Purge() + + log.Info("Rewind ancient data", "number", head) + return nil } // InsertReceiptChain attempts to complete an already existing header chain with // transaction and receipt data. -func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) { +func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain []types.Receipts, ancientLimit uint64) (int, error) { bc.wg.Add(1) defer bc.wg.Done() + var ( + ancientBlocks, liveBlocks types.Blocks + ancientReceipts, liveReceipts []types.Receipts + ) // Do a sanity check that the provided chain is actually ordered and linked - for i := 1; i < len(blockChain); i++ { - if blockChain[i].NumberU64() != blockChain[i-1].NumberU64()+1 || blockChain[i].ParentHash() != blockChain[i-1].Hash() { - log.Error("Non contiguous receipt insert", "number", blockChain[i].Number(), "hash", blockChain[i].Hash(), "parent", blockChain[i].ParentHash(), - "prevnumber", blockChain[i-1].Number(), "prevhash", blockChain[i-1].Hash()) - return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x…], item %d is #%d [%x…] (parent [%x…])", i-1, blockChain[i-1].NumberU64(), - blockChain[i-1].Hash().Bytes()[:4], i, blockChain[i].NumberU64(), blockChain[i].Hash().Bytes()[:4], blockChain[i].ParentHash().Bytes()[:4]) + for i := 0; i < len(blockChain); i++ { + if i != 0 { + if blockChain[i].NumberU64() != blockChain[i-1].NumberU64()+1 || blockChain[i].ParentHash() != blockChain[i-1].Hash() { + log.Error("Non contiguous receipt insert", "number", blockChain[i].Number(), "hash", blockChain[i].Hash(), "parent", blockChain[i].ParentHash(), + "prevnumber", blockChain[i-1].Number(), "prevhash", blockChain[i-1].Hash()) + return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x…], item %d is #%d [%x…] (parent [%x…])", i-1, blockChain[i-1].NumberU64(), + blockChain[i-1].Hash().Bytes()[:4], i, blockChain[i].NumberU64(), blockChain[i].Hash().Bytes()[:4], blockChain[i].ParentHash().Bytes()[:4]) + } + } + if blockChain[i].NumberU64() <= ancientLimit { + ancientBlocks, ancientReceipts = append(ancientBlocks, blockChain[i]), append(ancientReceipts, receiptChain[i]) + } else { + liveBlocks, liveReceipts = append(liveBlocks, blockChain[i]), append(liveReceipts, receiptChain[i]) } } var ( stats = struct{ processed, ignored int32 }{} start = time.Now() - bytes = 0 - batch = bc.db.NewBatch() + size = 0 ) - for i, block := range blockChain { - receipts := receiptChain[i] - // Short circuit insertion if shutting down or processing failed - if atomic.LoadInt32(&bc.procInterrupt) == 1 { - return 0, nil - } - // Short circuit if the owner header is unknown - if !bc.HasHeader(block.Hash(), block.NumberU64()) { - return i, fmt.Errorf("containing header #%d [%x…] unknown", block.Number(), block.Hash().Bytes()[:4]) - } - // Skip if the entire data is already known - if bc.HasBlock(block.Hash(), block.NumberU64()) { - stats.ignored++ - continue - } - // Compute all the non-consensus fields of the receipts - if err := receipts.DeriveFields(bc.chainConfig, block.Hash(), block.NumberU64(), block.Transactions()); err != nil { - return i, fmt.Errorf("failed to derive receipts data: %v", err) - } - // Write all the data out into the database - rawdb.WriteBody(batch, block.Hash(), block.NumberU64(), block.Body()) - rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receipts) - rawdb.WriteTxLookupEntries(batch, block) - - stats.processed++ - - if batch.ValueSize() >= ethdb.IdealBatchSize { - if err := batch.Write(); err != nil { - return 0, err + // updateHead updates the head fast sync block if the inserted blocks are better + // and returns a indicator whether the inserted blocks are canonical. + updateHead := func(head *types.Block) bool { + var isCanonical bool + bc.chainmu.Lock() + if td := bc.GetTd(head.Hash(), head.NumberU64()); td != nil { // Rewind may have occurred, skip in that case + currentFastBlock := bc.CurrentFastBlock() + if bc.GetTd(currentFastBlock.Hash(), currentFastBlock.NumberU64()).Cmp(td) < 0 { + rawdb.WriteHeadFastBlockHash(bc.db, head.Hash()) + bc.currentFastBlock.Store(head) + isCanonical = true } - bytes += batch.ValueSize() - batch.Reset() } + bc.chainmu.Unlock() + return isCanonical } - if batch.ValueSize() > 0 { - bytes += batch.ValueSize() + // writeAncient writes blockchain and corresponding receipt chain into ancient store. + // + // this function only accepts canonical chain data. All side chain will be reverted + // eventually. + writeAncient := func(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) { + var ( + previous = bc.CurrentFastBlock() + batch = bc.db.NewBatch() + ) + // If any error occurs before updating the head or we are inserting a side chain, + // all the data written this time wll be rolled back. + defer func() { + if previous != nil { + if err := bc.truncateAncient(previous.NumberU64()); err != nil { + log.Crit("Truncate ancient store failed", "err", err) + } + } + }() + for i, block := range blockChain { + // Short circuit insertion if shutting down or processing failed + if atomic.LoadInt32(&bc.procInterrupt) == 1 { + return 0, errInsertionInterrupted + } + // Short circuit insertion if it is required(used in testing only) + if bc.terminateInsert != nil && bc.terminateInsert(block.Hash(), block.NumberU64()) { + return i, errors.New("insertion is terminated for testing purpose") + } + // Short circuit if the owner header is unknown + if !bc.HasHeader(block.Hash(), block.NumberU64()) { + return i, fmt.Errorf("containing header #%d [%x…] unknown", block.Number(), block.Hash().Bytes()[:4]) + } + // Compute all the non-consensus fields of the receipts + if err := receiptChain[i].DeriveFields(bc.chainConfig, block.Hash(), block.NumberU64(), block.Transactions()); err != nil { + return i, fmt.Errorf("failed to derive receipts data: %v", err) + } + // Initialize freezer with genesis block first + if frozen, err := bc.db.Ancients(); err == nil && frozen == 0 && block.NumberU64() == 1 { + genesisBlock := rawdb.ReadBlock(bc.db, rawdb.ReadCanonicalHash(bc.db, 0), 0) + size += rawdb.WriteAncientBlock(bc.db, genesisBlock, nil, genesisBlock.Difficulty()) + } + // Flush data into ancient store. + size += rawdb.WriteAncientBlock(bc.db, block, receiptChain[i], bc.GetTd(block.Hash(), block.NumberU64())) + rawdb.WriteTxLookupEntries(batch, block) + + stats.processed++ + } + // Flush all tx-lookup index data. + size += batch.ValueSize() if err := batch.Write(); err != nil { return 0, err } - } + batch.Reset() - // Update the head fast sync block if better - bc.chainmu.Lock() - head := blockChain[len(blockChain)-1] - if td := bc.GetTd(head.Hash(), head.NumberU64()); td != nil { // Rewind may have occurred, skip in that case - currentFastBlock := bc.CurrentFastBlock() - if bc.GetTd(currentFastBlock.Hash(), currentFastBlock.NumberU64()).Cmp(td) < 0 { - rawdb.WriteHeadFastBlockHash(bc.db, head.Hash()) - bc.currentFastBlock.Store(head) + // Sync the ancient store explicitly to ensure all data has been flushed to disk. + if err := bc.db.Sync(); err != nil { + return 0, err + } + if !updateHead(blockChain[len(blockChain)-1]) { + return 0, errors.New("side blocks can't be accepted as the ancient chain data") + } + previous = nil // disable rollback explicitly + + // Remove the ancient data from the active store + cleanGenesis := len(blockChain) > 0 && blockChain[0].NumberU64() == 1 + if cleanGenesis { + // Migrate genesis block to ancient store too. + rawdb.DeleteBlockWithoutNumber(batch, rawdb.ReadCanonicalHash(bc.db, 0), 0) + rawdb.DeleteCanonicalHash(batch, 0) + } + // Wipe out canonical block data. + for _, block := range blockChain { + rawdb.DeleteBlockWithoutNumber(batch, block.Hash(), block.NumberU64()) + rawdb.DeleteCanonicalHash(batch, block.NumberU64()) + } + if err := batch.Write(); err != nil { + return 0, err + } + batch.Reset() + // Wipe out side chain too. + for _, block := range blockChain { + for _, hash := range rawdb.ReadAllHashes(bc.db, block.NumberU64()) { + rawdb.DeleteBlock(batch, hash, block.NumberU64()) + } + } + if err := batch.Write(); err != nil { + return 0, err + } + return 0, nil + } + // writeLive writes blockchain and corresponding receipt chain into active store. + writeLive := func(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) { + batch := bc.db.NewBatch() + for i, block := range blockChain { + // Short circuit insertion if shutting down or processing failed + if atomic.LoadInt32(&bc.procInterrupt) == 1 { + return 0, errInsertionInterrupted + } + // Short circuit if the owner header is unknown + if !bc.HasHeader(block.Hash(), block.NumberU64()) { + return i, fmt.Errorf("containing header #%d [%x…] unknown", block.Number(), block.Hash().Bytes()[:4]) + } + if bc.HasBlock(block.Hash(), block.NumberU64()) { + stats.ignored++ + continue + } + // Compute all the non-consensus fields of the receipts + if err := receiptChain[i].DeriveFields(bc.chainConfig, block.Hash(), block.NumberU64(), block.Transactions()); err != nil { + return i, fmt.Errorf("failed to derive receipts data: %v", err) + } + // Write all the data out into the database + rawdb.WriteBody(batch, block.Hash(), block.NumberU64(), block.Body()) + rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receiptChain[i]) + rawdb.WriteTxLookupEntries(batch, block) + + stats.processed++ + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return 0, err + } + size += batch.ValueSize() + batch.Reset() + } + } + if batch.ValueSize() > 0 { + size += batch.ValueSize() + if err := batch.Write(); err != nil { + return 0, err + } + } + updateHead(blockChain[len(blockChain)-1]) + return 0, nil + } + // Write downloaded chain data and corresponding receipt chain data. + if len(ancientBlocks) > 0 { + if n, err := writeAncient(ancientBlocks, ancientReceipts); err != nil { + if err == errInsertionInterrupted { + return 0, nil + } + return n, err + } + } + if len(liveBlocks) > 0 { + if n, err := writeLive(liveBlocks, liveReceipts); err != nil { + if err == errInsertionInterrupted { + return 0, nil + } + return n, err } } - bc.chainmu.Unlock() + head := blockChain[len(blockChain)-1] context := []interface{}{ "count", stats.processed, "elapsed", common.PrettyDuration(time.Since(start)), "number", head.Number(), "hash", head.Hash(), "age", common.PrettyAge(time.Unix(int64(head.Time()), 0)), - "size", common.StorageSize(bytes), + "size", common.StorageSize(size), } if stats.ignored > 0 { context = append(context, []interface{}{"ignored", stats.ignored}...) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 70e3207f58..7b1a9a54f4 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -18,8 +18,10 @@ package core import ( "fmt" + "io/ioutil" "math/big" "math/rand" + "os" "sync" "testing" "time" @@ -33,7 +35,6 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/params" ) @@ -639,7 +640,27 @@ func TestFastVsFullChains(t *testing.T) { if n, err := fast.InsertHeaderChain(headers, 1); err != nil { t.Fatalf("failed to insert header %d: %v", n, err) } - if n, err := fast.InsertReceiptChain(blocks, receipts); err != nil { + if n, err := fast.InsertReceiptChain(blocks, receipts, 0); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + // Freezer style fast import the chain. + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "") + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(len(blocks)/2)); err != nil { t.Fatalf("failed to insert receipt %d: %v", n, err) } // Iterate over all chain data components, and cross reference @@ -647,26 +668,35 @@ func TestFastVsFullChains(t *testing.T) { num, hash := blocks[i].NumberU64(), blocks[i].Hash() if ftd, atd := fast.GetTdByHash(hash), archive.GetTdByHash(hash); ftd.Cmp(atd) != 0 { - t.Errorf("block #%d [%x]: td mismatch: have %v, want %v", num, hash, ftd, atd) + t.Errorf("block #%d [%x]: td mismatch: fastdb %v, archivedb %v", num, hash, ftd, atd) + } + if antd, artd := ancient.GetTdByHash(hash), archive.GetTdByHash(hash); antd.Cmp(artd) != 0 { + t.Errorf("block #%d [%x]: td mismatch: ancientdb %v, archivedb %v", num, hash, antd, artd) } if fheader, aheader := fast.GetHeaderByHash(hash), archive.GetHeaderByHash(hash); fheader.Hash() != aheader.Hash() { - t.Errorf("block #%d [%x]: header mismatch: have %v, want %v", num, hash, fheader, aheader) + t.Errorf("block #%d [%x]: header mismatch: fastdb %v, archivedb %v", num, hash, fheader, aheader) } - if fblock, ablock := fast.GetBlockByHash(hash), archive.GetBlockByHash(hash); fblock.Hash() != ablock.Hash() { - t.Errorf("block #%d [%x]: block mismatch: have %v, want %v", num, hash, fblock, ablock) - } else if types.DeriveSha(fblock.Transactions()) != types.DeriveSha(ablock.Transactions()) { - t.Errorf("block #%d [%x]: transactions mismatch: have %v, want %v", num, hash, fblock.Transactions(), ablock.Transactions()) - } else if types.CalcUncleHash(fblock.Uncles()) != types.CalcUncleHash(ablock.Uncles()) { - t.Errorf("block #%d [%x]: uncles mismatch: have %v, want %v", num, hash, fblock.Uncles(), ablock.Uncles()) + if anheader, arheader := ancient.GetHeaderByHash(hash), archive.GetHeaderByHash(hash); anheader.Hash() != arheader.Hash() { + t.Errorf("block #%d [%x]: header mismatch: ancientdb %v, archivedb %v", num, hash, anheader, arheader) } - if freceipts, areceipts := rawdb.ReadReceipts(fastDb, hash, *rawdb.ReadHeaderNumber(fastDb, hash), fast.Config()), rawdb.ReadReceipts(archiveDb, hash, *rawdb.ReadHeaderNumber(archiveDb, hash), archive.Config()); types.DeriveSha(freceipts) != types.DeriveSha(areceipts) { - t.Errorf("block #%d [%x]: receipts mismatch: have %v, want %v", num, hash, freceipts, areceipts) + if fblock, arblock, anblock := fast.GetBlockByHash(hash), archive.GetBlockByHash(hash), ancient.GetBlockByHash(hash); fblock.Hash() != arblock.Hash() || anblock.Hash() != arblock.Hash() { + t.Errorf("block #%d [%x]: block mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock, anblock, arblock) + } else if types.DeriveSha(fblock.Transactions()) != types.DeriveSha(arblock.Transactions()) || types.DeriveSha(anblock.Transactions()) != types.DeriveSha(arblock.Transactions()) { + t.Errorf("block #%d [%x]: transactions mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock.Transactions(), anblock.Transactions(), arblock.Transactions()) + } else if types.CalcUncleHash(fblock.Uncles()) != types.CalcUncleHash(arblock.Uncles()) || types.CalcUncleHash(anblock.Uncles()) != types.CalcUncleHash(arblock.Uncles()) { + t.Errorf("block #%d [%x]: uncles mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock.Uncles(), anblock, arblock.Uncles()) + } + if freceipts, anreceipts, areceipts := rawdb.ReadReceipts(fastDb, hash, *rawdb.ReadHeaderNumber(fastDb, hash), fast.Config()), rawdb.ReadReceipts(ancientDb, hash, *rawdb.ReadHeaderNumber(ancientDb, hash), fast.Config()), rawdb.ReadReceipts(archiveDb, hash, *rawdb.ReadHeaderNumber(archiveDb, hash), fast.Config()); types.DeriveSha(freceipts) != types.DeriveSha(areceipts) { + t.Errorf("block #%d [%x]: receipts mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, freceipts, anreceipts, areceipts) } } // Check that the canonical chains are the same between the databases for i := 0; i < len(blocks)+1; i++ { if fhash, ahash := rawdb.ReadCanonicalHash(fastDb, uint64(i)), rawdb.ReadCanonicalHash(archiveDb, uint64(i)); fhash != ahash { - t.Errorf("block #%d: canonical hash mismatch: have %v, want %v", i, fhash, ahash) + t.Errorf("block #%d: canonical hash mismatch: fastdb %v, archivedb %v", i, fhash, ahash) + } + if anhash, arhash := rawdb.ReadCanonicalHash(ancientDb, uint64(i)), rawdb.ReadCanonicalHash(archiveDb, uint64(i)); anhash != arhash { + t.Errorf("block #%d: canonical hash mismatch: ancientdb %v, archivedb %v", i, anhash, arhash) } } } @@ -730,13 +760,40 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { if n, err := fast.InsertHeaderChain(headers, 1); err != nil { t.Fatalf("failed to insert header %d: %v", n, err) } - if n, err := fast.InsertReceiptChain(blocks, receipts); err != nil { + if n, err := fast.InsertReceiptChain(blocks, receipts, 0); err != nil { t.Fatalf("failed to insert receipt %d: %v", n, err) } assert(t, "fast", fast, height, height, 0) fast.Rollback(remove) assert(t, "fast", fast, height/2, height/2, 0) + // Import the chain as a ancient-first node and ensure all pointers are updated + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "") + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + assert(t, "ancient", ancient, height, height, 0) + ancient.Rollback(remove) + assert(t, "ancient", ancient, height/2, height/2, 0) + if frozen, err := ancientDb.Ancients(); err != nil || frozen != height/2+1 { + t.Fatalf("failed to truncate ancient store, want %v, have %v", height/2+1, frozen) + } + // Import the chain as a light node and ensure all pointers are updated lightDb := rawdb.NewMemoryDatabase() gspec.MustCommit(lightDb) @@ -918,7 +975,7 @@ func TestLogRebirth(t *testing.T) { var ( key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") addr1 = crypto.PubkeyToAddress(key1.PublicKey) - db = memorydb.New() + db = rawdb.NewMemoryDatabase() // this code generates a log code = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") @@ -1040,7 +1097,7 @@ func TestSideLogRebirth(t *testing.T) { var ( key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") addr1 = crypto.PubkeyToAddress(key1.PublicKey) - db = memorydb.New() + db = rawdb.NewMemoryDatabase() // this code generates a log code = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") @@ -1564,6 +1621,122 @@ func TestLargeReorgTrieGC(t *testing.T) { } } +func TestBlockchainRecovery(t *testing.T) { + // Configure and generate a sample block chain + var ( + gendb = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000) + gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{address: {Balance: funds}}} + genesis = gspec.MustCommit(gendb) + ) + height := uint64(1024) + blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), nil) + + // Import the chain as a ancient-first node and ensure all pointers are updated + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "") + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + + headers := make([]*types.Header, len(blocks)) + for i, block := range blocks { + headers[i] = block.Header() + } + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + ancient.Stop() + + // Destroy head fast block manually + midBlock := blocks[len(blocks)/2] + rawdb.WriteHeadFastBlockHash(ancientDb, midBlock.Hash()) + + // Reopen broken blockchain again + ancient, _ = NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + if num := ancient.CurrentBlock().NumberU64(); num != 0 { + t.Errorf("head block mismatch: have #%v, want #%v", num, 0) + } + if num := ancient.CurrentFastBlock().NumberU64(); num != midBlock.NumberU64() { + t.Errorf("head fast-block mismatch: have #%v, want #%v", num, midBlock.NumberU64()) + } + if num := ancient.CurrentHeader().Number.Uint64(); num != midBlock.NumberU64() { + t.Errorf("head header mismatch: have #%v, want #%v", num, midBlock.NumberU64()) + } +} + +func TestIncompleteAncientReceiptChainInsertion(t *testing.T) { + // Configure and generate a sample block chain + var ( + gendb = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000) + gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{address: {Balance: funds}}} + genesis = gspec.MustCommit(gendb) + ) + height := uint64(1024) + blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), nil) + + // Import the chain as a ancient-first node and ensure all pointers are updated + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "") + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + + headers := make([]*types.Header, len(blocks)) + for i, block := range blocks { + headers[i] = block.Header() + } + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + // Abort ancient receipt chain insertion deliberately + ancient.terminateInsert = func(hash common.Hash, number uint64) bool { + if number == blocks[len(blocks)/2].NumberU64() { + return true + } + return false + } + previousFastBlock := ancient.CurrentFastBlock() + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err == nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + if ancient.CurrentFastBlock().NumberU64() != previousFastBlock.NumberU64() { + t.Fatalf("failed to rollback ancient data, want %d, have %d", previousFastBlock.NumberU64(), ancient.CurrentFastBlock().NumberU64()) + } + if frozen, err := ancient.db.Ancients(); err != nil || frozen != 1 { + t.Fatalf("failed to truncate ancient data") + } + ancient.terminateInsert = nil + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + if ancient.CurrentFastBlock().NumberU64() != blocks[len(blocks)-1].NumberU64() { + t.Fatalf("failed to insert ancient recept chain after rollback") + } +} + // Tests that importing a very large side fork, which is larger than the canon chain, // but where the difficulty per block is kept low: this means that it will not // overtake the 'canon' chain until after it's passed canon by about 200 blocks. @@ -1764,7 +1937,7 @@ func testInsertKnownChainData(t *testing.T, typ string) { if err != nil { return err } - _, err = chain.InsertReceiptChain(blocks, receipts) + _, err = chain.InsertReceiptChain(blocks, receipts, 0) return err } asserter = func(t *testing.T, block *types.Block) { @@ -2019,14 +2192,12 @@ func BenchmarkBlockChain_1x1000ValueTransferToNonexisting(b *testing.B) { numTxs = 1000 numBlocks = 1 ) - recipientFn := func(nonce uint64) common.Address { return common.BigToAddress(big.NewInt(0).SetUint64(1337 + nonce)) } dataFn := func(nonce uint64) []byte { return nil } - benchmarkLargeNumberOfValueToNonexisting(b, numTxs, numBlocks, recipientFn, dataFn) } @@ -2044,7 +2215,6 @@ func BenchmarkBlockChain_1x1000ValueTransferToExisting(b *testing.B) { dataFn := func(nonce uint64) []byte { return nil } - benchmarkLargeNumberOfValueToNonexisting(b, numTxs, numBlocks, recipientFn, dataFn) } @@ -2062,6 +2232,5 @@ func BenchmarkBlockChain_1x1000Executions(b *testing.B) { dataFn := func(nonce uint64) []byte { return nil } - benchmarkLargeNumberOfValueToNonexisting(b, numTxs, numBlocks, recipientFn, dataFn) } diff --git a/core/headerchain.go b/core/headerchain.go index d0c1987fb5..659141fd13 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -453,33 +453,56 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.Header) { hc.currentHeaderHash = head.Hash() } -// DeleteCallback is a callback function that is called by SetHead before -// each header is deleted. -type DeleteCallback func(ethdb.Writer, common.Hash, uint64) +type ( + // UpdateHeadBlocksCallback is a callback function that is called by SetHead + // before head header is updated. + UpdateHeadBlocksCallback func(ethdb.KeyValueWriter, *types.Header) + + // DeleteBlockContentCallback is a callback function that is called by SetHead + // before each header is deleted. + DeleteBlockContentCallback func(ethdb.KeyValueWriter, common.Hash, uint64) +) // SetHead rewinds the local chain to a new head. Everything above the new head // will be deleted and the new one set. -func (hc *HeaderChain) SetHead(head uint64, delFn DeleteCallback) { - height := uint64(0) - - if hdr := hc.CurrentHeader(); hdr != nil { - height = hdr.Number.Uint64() - } - batch := hc.chainDb.NewBatch() +func (hc *HeaderChain) SetHead(head uint64, updateFn UpdateHeadBlocksCallback, delFn DeleteBlockContentCallback) { + var ( + parentHash common.Hash + batch = hc.chainDb.NewBatch() + ) for hdr := hc.CurrentHeader(); hdr != nil && hdr.Number.Uint64() > head; hdr = hc.CurrentHeader() { - hash := hdr.Hash() - num := hdr.Number.Uint64() + hash, num := hdr.Hash(), hdr.Number.Uint64() + + // Rewind block chain to new head. + parent := hc.GetHeader(hdr.ParentHash, num-1) + if parent == nil { + parent = hc.genesisHeader + } + parentHash = hdr.ParentHash + // Notably, since geth has the possibility for setting the head to a low + // height which is even lower than ancient head. + // In order to ensure that the head is always no higher than the data in + // the database(ancient store or active store), we need to update head + // first then remove the relative data from the database. + // + // Update head first(head fast block, head full block) before deleting the data. + if updateFn != nil { + updateFn(hc.chainDb, parent) + } + // Update head header then. + rawdb.WriteHeadHeaderHash(hc.chainDb, parentHash) + + // Remove the relative data from the database. if delFn != nil { delFn(batch, hash, num) } + // Rewind header chain to new head. rawdb.DeleteHeader(batch, hash, num) rawdb.DeleteTd(batch, hash, num) + rawdb.DeleteCanonicalHash(batch, num) - hc.currentHeader.Store(hc.GetHeader(hdr.ParentHash, hdr.Number.Uint64()-1)) - } - // Roll back the canonical chain numbering - for i := height; i > head; i-- { - rawdb.DeleteCanonicalHash(batch, i) + hc.currentHeader.Store(parent) + hc.currentHeaderHash = parentHash } batch.Write() @@ -487,13 +510,6 @@ func (hc *HeaderChain) SetHead(head uint64, delFn DeleteCallback) { hc.headerCache.Purge() hc.tdCache.Purge() hc.numberCache.Purge() - - if hc.CurrentHeader() == nil { - hc.currentHeader.Store(hc.genesisHeader) - } - hc.currentHeaderHash = hc.CurrentHeader().Hash() - - rawdb.WriteHeadHeaderHash(hc.chainDb, hc.currentHeaderHash) } // SetGenesis sets a new genesis block header for the chain diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 103f18f78c..681e6e9171 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -30,10 +30,17 @@ import ( ) // ReadCanonicalHash retrieves the hash assigned to a canonical block number. -func ReadCanonicalHash(db ethdb.AncientReader, number uint64) common.Hash { - data, _ := db.Ancient("hashes", number) +func ReadCanonicalHash(db ethdb.Reader, number uint64) common.Hash { + data, _ := db.Ancient(freezerHashTable, number) if len(data) == 0 { data, _ = db.Get(headerHashKey(number)) + // In the background freezer is moving data from leveldb to flatten files. + // So during the first check for ancient db, the data is not yet in there, + // but when we reach into leveldb, the data was already moved. That would + // result in a not found error. + if len(data) == 0 { + data, _ = db.Ancient(freezerHashTable, number) + } } if len(data) == 0 { return common.Hash{} @@ -42,29 +49,28 @@ func ReadCanonicalHash(db ethdb.AncientReader, number uint64) common.Hash { } // WriteCanonicalHash stores the hash assigned to a canonical block number. -func WriteCanonicalHash(db ethdb.Writer, hash common.Hash, number uint64) { +func WriteCanonicalHash(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { if err := db.Put(headerHashKey(number), hash.Bytes()); err != nil { log.Crit("Failed to store number to hash mapping", "err", err) } } // DeleteCanonicalHash removes the number to hash canonical mapping. -func DeleteCanonicalHash(db ethdb.Writer, number uint64) { +func DeleteCanonicalHash(db ethdb.KeyValueWriter, number uint64) { if err := db.Delete(headerHashKey(number)); err != nil { log.Crit("Failed to delete number to hash mapping", "err", err) } } -// readAllHashes retrieves all the hashes assigned to blocks at a certain heights, +// ReadAllHashes retrieves all the hashes assigned to blocks at a certain heights, // both canonical and reorged forks included. -// -// This method is a helper for the chain reader. It should never be exposed to the -// outside world. -func readAllHashes(db ethdb.Iteratee, number uint64) []common.Hash { +func ReadAllHashes(db ethdb.Iteratee, number uint64) []common.Hash { prefix := headerKeyPrefix(number) hashes := make([]common.Hash, 0, 1) it := db.NewIteratorWithPrefix(prefix) + defer it.Release() + for it.Next() { if key := it.Key(); len(key) == len(prefix)+32 { hashes = append(hashes, common.BytesToHash(key[len(key)-32:])) @@ -74,7 +80,7 @@ func readAllHashes(db ethdb.Iteratee, number uint64) []common.Hash { } // ReadHeaderNumber returns the header number assigned to a hash. -func ReadHeaderNumber(db ethdb.Reader, hash common.Hash) *uint64 { +func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 { data, _ := db.Get(headerNumberKey(hash)) if len(data) != 8 { return nil @@ -83,8 +89,15 @@ func ReadHeaderNumber(db ethdb.Reader, hash common.Hash) *uint64 { return &number } +// DeleteHeaderNumber removes hash to number mapping. +func DeleteHeaderNumber(db ethdb.KeyValueWriter, hash common.Hash) { + if err := db.Delete(headerNumberKey(hash)); err != nil { + log.Crit("Failed to delete hash to number mapping", "err", err) + } +} + // ReadHeadHeaderHash retrieves the hash of the current canonical head header. -func ReadHeadHeaderHash(db ethdb.Reader) common.Hash { +func ReadHeadHeaderHash(db ethdb.KeyValueReader) common.Hash { data, _ := db.Get(headHeaderKey) if len(data) == 0 { return common.Hash{} @@ -93,14 +106,14 @@ func ReadHeadHeaderHash(db ethdb.Reader) common.Hash { } // WriteHeadHeaderHash stores the hash of the current canonical head header. -func WriteHeadHeaderHash(db ethdb.Writer, hash common.Hash) { +func WriteHeadHeaderHash(db ethdb.KeyValueWriter, hash common.Hash) { if err := db.Put(headHeaderKey, hash.Bytes()); err != nil { log.Crit("Failed to store last header's hash", "err", err) } } // ReadHeadBlockHash retrieves the hash of the current canonical head block. -func ReadHeadBlockHash(db ethdb.Reader) common.Hash { +func ReadHeadBlockHash(db ethdb.KeyValueReader) common.Hash { data, _ := db.Get(headBlockKey) if len(data) == 0 { return common.Hash{} @@ -109,14 +122,14 @@ func ReadHeadBlockHash(db ethdb.Reader) common.Hash { } // WriteHeadBlockHash stores the head block's hash. -func WriteHeadBlockHash(db ethdb.Writer, hash common.Hash) { +func WriteHeadBlockHash(db ethdb.KeyValueWriter, hash common.Hash) { if err := db.Put(headBlockKey, hash.Bytes()); err != nil { log.Crit("Failed to store last block's hash", "err", err) } } // ReadHeadFastBlockHash retrieves the hash of the current fast-sync head block. -func ReadHeadFastBlockHash(db ethdb.Reader) common.Hash { +func ReadHeadFastBlockHash(db ethdb.KeyValueReader) common.Hash { data, _ := db.Get(headFastBlockKey) if len(data) == 0 { return common.Hash{} @@ -125,7 +138,7 @@ func ReadHeadFastBlockHash(db ethdb.Reader) common.Hash { } // WriteHeadFastBlockHash stores the hash of the current fast-sync head block. -func WriteHeadFastBlockHash(db ethdb.Writer, hash common.Hash) { +func WriteHeadFastBlockHash(db ethdb.KeyValueWriter, hash common.Hash) { if err := db.Put(headFastBlockKey, hash.Bytes()); err != nil { log.Crit("Failed to store last fast block's hash", "err", err) } @@ -133,7 +146,7 @@ func WriteHeadFastBlockHash(db ethdb.Writer, hash common.Hash) { // ReadFastTrieProgress retrieves the number of tries nodes fast synced to allow // reporting correct numbers across restarts. -func ReadFastTrieProgress(db ethdb.Reader) uint64 { +func ReadFastTrieProgress(db ethdb.KeyValueReader) uint64 { data, _ := db.Get(fastTrieProgressKey) if len(data) == 0 { return 0 @@ -143,24 +156,31 @@ func ReadFastTrieProgress(db ethdb.Reader) uint64 { // WriteFastTrieProgress stores the fast sync trie process counter to support // retrieving it across restarts. -func WriteFastTrieProgress(db ethdb.Writer, count uint64) { +func WriteFastTrieProgress(db ethdb.KeyValueWriter, count uint64) { if err := db.Put(fastTrieProgressKey, new(big.Int).SetUint64(count).Bytes()); err != nil { log.Crit("Failed to store fast sync trie progress", "err", err) } } // ReadHeaderRLP retrieves a block header in its raw RLP database encoding. -func ReadHeaderRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Ancient("headers", number) +func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient(freezerHeaderTable, number) if len(data) == 0 { data, _ = db.Get(headerKey(number, hash)) + // In the background freezer is moving data from leveldb to flatten files. + // So during the first check for ancient db, the data is not yet in there, + // but when we reach into leveldb, the data was already moved. That would + // result in a not found error. + if len(data) == 0 { + data, _ = db.Ancient(freezerHeaderTable, number) + } } return data } // HasHeader verifies the existence of a block header corresponding to the hash. -func HasHeader(db ethdb.AncientReader, hash common.Hash, number uint64) bool { - if has, err := db.Ancient("hashes", number); err == nil && common.BytesToHash(has) == hash { +func HasHeader(db ethdb.Reader, hash common.Hash, number uint64) bool { + if has, err := db.Ancient(freezerHashTable, number); err == nil && common.BytesToHash(has) == hash { return true } if has, err := db.Has(headerKey(number, hash)); !has || err != nil { @@ -170,7 +190,7 @@ func HasHeader(db ethdb.AncientReader, hash common.Hash, number uint64) bool { } // ReadHeader retrieves the block header corresponding to the hash. -func ReadHeader(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Header { +func ReadHeader(db ethdb.Reader, hash common.Hash, number uint64) *types.Header { data := ReadHeaderRLP(db, hash, number) if len(data) == 0 { return nil @@ -185,7 +205,7 @@ func ReadHeader(db ethdb.AncientReader, hash common.Hash, number uint64) *types. // WriteHeader stores a block header into the database and also stores the hash- // to-number mapping. -func WriteHeader(db ethdb.Writer, header *types.Header) { +func WriteHeader(db ethdb.KeyValueWriter, header *types.Header) { // Write the hash -> number mapping var ( hash = header.Hash() @@ -208,7 +228,7 @@ func WriteHeader(db ethdb.Writer, header *types.Header) { } // DeleteHeader removes all block header data associated with a hash. -func DeleteHeader(db ethdb.Writer, hash common.Hash, number uint64) { +func DeleteHeader(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { deleteHeaderWithoutNumber(db, hash, number) if err := db.Delete(headerNumberKey(hash)); err != nil { log.Crit("Failed to delete hash to number mapping", "err", err) @@ -217,31 +237,38 @@ func DeleteHeader(db ethdb.Writer, hash common.Hash, number uint64) { // deleteHeaderWithoutNumber removes only the block header but does not remove // the hash to number mapping. -func deleteHeaderWithoutNumber(db ethdb.Writer, hash common.Hash, number uint64) { +func deleteHeaderWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { if err := db.Delete(headerKey(number, hash)); err != nil { log.Crit("Failed to delete header", "err", err) } } // ReadBodyRLP retrieves the block body (transactions and uncles) in RLP encoding. -func ReadBodyRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Ancient("bodies", number) +func ReadBodyRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient(freezerBodiesTable, number) if len(data) == 0 { data, _ = db.Get(blockBodyKey(number, hash)) + // In the background freezer is moving data from leveldb to flatten files. + // So during the first check for ancient db, the data is not yet in there, + // but when we reach into leveldb, the data was already moved. That would + // result in a not found error. + if len(data) == 0 { + data, _ = db.Ancient(freezerBodiesTable, number) + } } return data } // WriteBodyRLP stores an RLP encoded block body into the database. -func WriteBodyRLP(db ethdb.Writer, hash common.Hash, number uint64, rlp rlp.RawValue) { +func WriteBodyRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, rlp rlp.RawValue) { if err := db.Put(blockBodyKey(number, hash), rlp); err != nil { log.Crit("Failed to store block body", "err", err) } } // HasBody verifies the existence of a block body corresponding to the hash. -func HasBody(db ethdb.AncientReader, hash common.Hash, number uint64) bool { - if has, err := db.Ancient("hashes", number); err == nil && common.BytesToHash(has) == hash { +func HasBody(db ethdb.Reader, hash common.Hash, number uint64) bool { + if has, err := db.Ancient(freezerHashTable, number); err == nil && common.BytesToHash(has) == hash { return true } if has, err := db.Has(blockBodyKey(number, hash)); !has || err != nil { @@ -251,7 +278,7 @@ func HasBody(db ethdb.AncientReader, hash common.Hash, number uint64) bool { } // ReadBody retrieves the block body corresponding to the hash. -func ReadBody(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Body { +func ReadBody(db ethdb.Reader, hash common.Hash, number uint64) *types.Body { data := ReadBodyRLP(db, hash, number) if len(data) == 0 { return nil @@ -265,7 +292,7 @@ func ReadBody(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Bo } // WriteBody stores a block body into the database. -func WriteBody(db ethdb.Writer, hash common.Hash, number uint64, body *types.Body) { +func WriteBody(db ethdb.KeyValueWriter, hash common.Hash, number uint64, body *types.Body) { data, err := rlp.EncodeToBytes(body) if err != nil { log.Crit("Failed to RLP encode body", "err", err) @@ -274,23 +301,30 @@ func WriteBody(db ethdb.Writer, hash common.Hash, number uint64, body *types.Bod } // DeleteBody removes all block body data associated with a hash. -func DeleteBody(db ethdb.Writer, hash common.Hash, number uint64) { +func DeleteBody(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { if err := db.Delete(blockBodyKey(number, hash)); err != nil { log.Crit("Failed to delete block body", "err", err) } } // ReadTdRLP retrieves a block's total difficulty corresponding to the hash in RLP encoding. -func ReadTdRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Ancient("diffs", number) +func ReadTdRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient(freezerDifficultyTable, number) if len(data) == 0 { data, _ = db.Get(headerTDKey(number, hash)) + // In the background freezer is moving data from leveldb to flatten files. + // So during the first check for ancient db, the data is not yet in there, + // but when we reach into leveldb, the data was already moved. That would + // result in a not found error. + if len(data) == 0 { + data, _ = db.Ancient(freezerDifficultyTable, number) + } } return data } // ReadTd retrieves a block's total difficulty corresponding to the hash. -func ReadTd(db ethdb.AncientReader, hash common.Hash, number uint64) *big.Int { +func ReadTd(db ethdb.Reader, hash common.Hash, number uint64) *big.Int { data := ReadTdRLP(db, hash, number) if len(data) == 0 { return nil @@ -304,7 +338,7 @@ func ReadTd(db ethdb.AncientReader, hash common.Hash, number uint64) *big.Int { } // WriteTd stores the total difficulty of a block into the database. -func WriteTd(db ethdb.Writer, hash common.Hash, number uint64, td *big.Int) { +func WriteTd(db ethdb.KeyValueWriter, hash common.Hash, number uint64, td *big.Int) { data, err := rlp.EncodeToBytes(td) if err != nil { log.Crit("Failed to RLP encode block total difficulty", "err", err) @@ -315,7 +349,7 @@ func WriteTd(db ethdb.Writer, hash common.Hash, number uint64, td *big.Int) { } // DeleteTd removes all block total difficulty data associated with a hash. -func DeleteTd(db ethdb.Writer, hash common.Hash, number uint64) { +func DeleteTd(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { if err := db.Delete(headerTDKey(number, hash)); err != nil { log.Crit("Failed to delete block total difficulty", "err", err) } @@ -323,8 +357,8 @@ func DeleteTd(db ethdb.Writer, hash common.Hash, number uint64) { // HasReceipts verifies the existence of all the transaction receipts belonging // to a block. -func HasReceipts(db ethdb.AncientReader, hash common.Hash, number uint64) bool { - if has, err := db.Ancient("hashes", number); err == nil && common.BytesToHash(has) == hash { +func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool { + if has, err := db.Ancient(freezerHashTable, number); err == nil && common.BytesToHash(has) == hash { return true } if has, err := db.Has(blockReceiptsKey(number, hash)); !has || err != nil { @@ -334,10 +368,17 @@ func HasReceipts(db ethdb.AncientReader, hash common.Hash, number uint64) bool { } // ReadReceiptsRLP retrieves all the transaction receipts belonging to a block in RLP encoding. -func ReadReceiptsRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Ancient("receipts", number) +func ReadReceiptsRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { + data, _ := db.Ancient(freezerReceiptTable, number) if len(data) == 0 { data, _ = db.Get(blockReceiptsKey(number, hash)) + // In the background freezer is moving data from leveldb to flatten files. + // So during the first check for ancient db, the data is not yet in there, + // but when we reach into leveldb, the data was already moved. That would + // result in a not found error. + if len(data) == 0 { + data, _ = db.Ancient(freezerReceiptTable, number) + } } return data } @@ -345,7 +386,7 @@ func ReadReceiptsRLP(db ethdb.AncientReader, hash common.Hash, number uint64) rl // ReadRawReceipts retrieves all the transaction receipts belonging to a block. // The receipt metadata fields are not guaranteed to be populated, so they // should not be used. Use ReadReceipts instead if the metadata is needed. -func ReadRawReceipts(db ethdb.AncientReader, hash common.Hash, number uint64) types.Receipts { +func ReadRawReceipts(db ethdb.Reader, hash common.Hash, number uint64) types.Receipts { // Retrieve the flattened receipt slice data := ReadReceiptsRLP(db, hash, number) if len(data) == 0 { @@ -371,7 +412,7 @@ func ReadRawReceipts(db ethdb.AncientReader, hash common.Hash, number uint64) ty // The current implementation populates these metadata fields by reading the receipts' // corresponding block body, so if the block body is not found it will return nil even // if the receipt itself is stored. -func ReadReceipts(db ethdb.AncientReader, hash common.Hash, number uint64, config *params.ChainConfig) types.Receipts { +func ReadReceipts(db ethdb.Reader, hash common.Hash, number uint64, config *params.ChainConfig) types.Receipts { // We're deriving many fields from the block body, retrieve beside the receipt receipts := ReadRawReceipts(db, hash, number) if receipts == nil { @@ -390,7 +431,7 @@ func ReadReceipts(db ethdb.AncientReader, hash common.Hash, number uint64, confi } // WriteReceipts stores all the transaction receipts belonging to a block. -func WriteReceipts(db ethdb.Writer, hash common.Hash, number uint64, receipts types.Receipts) { +func WriteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64, receipts types.Receipts) { // Convert the receipts into their storage form and serialize them storageReceipts := make([]*types.ReceiptForStorage, len(receipts)) for i, receipt := range receipts { @@ -407,7 +448,7 @@ func WriteReceipts(db ethdb.Writer, hash common.Hash, number uint64, receipts ty } // DeleteReceipts removes all receipt data associated with a block hash. -func DeleteReceipts(db ethdb.Writer, hash common.Hash, number uint64) { +func DeleteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { if err := db.Delete(blockReceiptsKey(number, hash)); err != nil { log.Crit("Failed to delete block receipts", "err", err) } @@ -419,7 +460,7 @@ func DeleteReceipts(db ethdb.Writer, hash common.Hash, number uint64) { // // Note, due to concurrent download of header and block body the header and thus // canonical hash can be stored in the database but the body data not (yet). -func ReadBlock(db ethdb.AncientReader, hash common.Hash, number uint64) *types.Block { +func ReadBlock(db ethdb.Reader, hash common.Hash, number uint64) *types.Block { header := ReadHeader(db, hash, number) if header == nil { return nil @@ -432,22 +473,53 @@ func ReadBlock(db ethdb.AncientReader, hash common.Hash, number uint64) *types.B } // WriteBlock serializes a block into the database, header and body separately. -func WriteBlock(db ethdb.Writer, block *types.Block) { +func WriteBlock(db ethdb.KeyValueWriter, block *types.Block) { WriteBody(db, block.Hash(), block.NumberU64(), block.Body()) WriteHeader(db, block.Header()) } +// WriteAncientBlock writes entire block data into ancient store and returns the total written size. +func WriteAncientBlock(db ethdb.AncientWriter, block *types.Block, receipts types.Receipts, td *big.Int) int { + // Encode all block components to RLP format. + headerBlob, err := rlp.EncodeToBytes(block.Header()) + if err != nil { + log.Crit("Failed to RLP encode block header", "err", err) + } + bodyBlob, err := rlp.EncodeToBytes(block.Body()) + if err != nil { + log.Crit("Failed to RLP encode body", "err", err) + } + storageReceipts := make([]*types.ReceiptForStorage, len(receipts)) + for i, receipt := range receipts { + storageReceipts[i] = (*types.ReceiptForStorage)(receipt) + } + receiptBlob, err := rlp.EncodeToBytes(storageReceipts) + if err != nil { + log.Crit("Failed to RLP encode block receipts", "err", err) + } + tdBlob, err := rlp.EncodeToBytes(td) + if err != nil { + log.Crit("Failed to RLP encode block total difficulty", "err", err) + } + // Write all blob to flatten files. + err = db.AppendAncient(block.NumberU64(), block.Hash().Bytes(), headerBlob, bodyBlob, receiptBlob, tdBlob) + if err != nil { + log.Crit("Failed to write block data to ancient store", "err", err) + } + return len(headerBlob) + len(bodyBlob) + len(receiptBlob) + len(tdBlob) + common.HashLength +} + // DeleteBlock removes all block data associated with a hash. -func DeleteBlock(db ethdb.Writer, hash common.Hash, number uint64) { +func DeleteBlock(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteReceipts(db, hash, number) DeleteHeader(db, hash, number) DeleteBody(db, hash, number) DeleteTd(db, hash, number) } -// deleteBlockWithoutNumber removes all block data associated with a hash, except +// DeleteBlockWithoutNumber removes all block data associated with a hash, except // the hash to number mapping. -func deleteBlockWithoutNumber(db ethdb.Writer, hash common.Hash, number uint64) { +func DeleteBlockWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteReceipts(db, hash, number) deleteHeaderWithoutNumber(db, hash, number) DeleteBody(db, hash, number) @@ -455,7 +527,7 @@ func deleteBlockWithoutNumber(db ethdb.Writer, hash common.Hash, number uint64) } // FindCommonAncestor returns the last common ancestor of two block headers -func FindCommonAncestor(db ethdb.AncientReader, a, b *types.Header) *types.Header { +func FindCommonAncestor(db ethdb.Reader, a, b *types.Header) *types.Header { for bn := b.Number.Uint64(); a.Number.Uint64() > bn; { a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1) if a == nil { diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index 666e3edff6..ed1f1bca6e 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -54,7 +54,7 @@ func ReadTxLookupEntry(db ethdb.Reader, hash common.Hash) *uint64 { // WriteTxLookupEntries stores a positional metadata for every transaction from // a block, enabling hash based transaction and receipt lookups. -func WriteTxLookupEntries(db ethdb.Writer, block *types.Block) { +func WriteTxLookupEntries(db ethdb.KeyValueWriter, block *types.Block) { for _, tx := range block.Transactions() { if err := db.Put(txLookupKey(tx.Hash()), block.Number().Bytes()); err != nil { log.Crit("Failed to store transaction lookup entry", "err", err) @@ -63,13 +63,13 @@ func WriteTxLookupEntries(db ethdb.Writer, block *types.Block) { } // DeleteTxLookupEntry removes all transaction data associated with a hash. -func DeleteTxLookupEntry(db ethdb.Writer, hash common.Hash) { +func DeleteTxLookupEntry(db ethdb.KeyValueWriter, hash common.Hash) { db.Delete(txLookupKey(hash)) } // ReadTransaction retrieves a specific transaction from the database, along with // its added positional metadata. -func ReadTransaction(db ethdb.AncientReader, hash common.Hash) (*types.Transaction, common.Hash, uint64, uint64) { +func ReadTransaction(db ethdb.Reader, hash common.Hash) (*types.Transaction, common.Hash, uint64, uint64) { blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { return nil, common.Hash{}, 0, 0 @@ -94,7 +94,7 @@ func ReadTransaction(db ethdb.AncientReader, hash common.Hash) (*types.Transacti // ReadReceipt retrieves a specific transaction receipt from the database, along with // its added positional metadata. -func ReadReceipt(db ethdb.AncientReader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { +func ReadReceipt(db ethdb.Reader, hash common.Hash, config *params.ChainConfig) (*types.Receipt, common.Hash, uint64, uint64) { // Retrieve the context of the receipt based on the transaction hash blockNumber := ReadTxLookupEntry(db, hash) if blockNumber == nil { @@ -117,13 +117,13 @@ func ReadReceipt(db ethdb.AncientReader, hash common.Hash, config *params.ChainC // ReadBloomBits retrieves the compressed bloom bit vector belonging to the given // section and bit index from the. -func ReadBloomBits(db ethdb.Reader, bit uint, section uint64, head common.Hash) ([]byte, error) { +func ReadBloomBits(db ethdb.KeyValueReader, bit uint, section uint64, head common.Hash) ([]byte, error) { return db.Get(bloomBitsKey(bit, section, head)) } // WriteBloomBits stores the compressed bloom bits vector belonging to the given // section and bit index. -func WriteBloomBits(db ethdb.Writer, bit uint, section uint64, head common.Hash, bits []byte) { +func WriteBloomBits(db ethdb.KeyValueWriter, bit uint, section uint64, head common.Hash, bits []byte) { if err := db.Put(bloomBitsKey(bit, section, head), bits); err != nil { log.Crit("Failed to store bloom bits", "err", err) } diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go index 1361b0d731..f8d09fbddf 100644 --- a/core/rawdb/accessors_metadata.go +++ b/core/rawdb/accessors_metadata.go @@ -27,7 +27,7 @@ import ( ) // ReadDatabaseVersion retrieves the version number of the database. -func ReadDatabaseVersion(db ethdb.Reader) *uint64 { +func ReadDatabaseVersion(db ethdb.KeyValueReader) *uint64 { var version uint64 enc, _ := db.Get(databaseVerisionKey) @@ -42,7 +42,7 @@ func ReadDatabaseVersion(db ethdb.Reader) *uint64 { } // WriteDatabaseVersion stores the version number of the database -func WriteDatabaseVersion(db ethdb.Writer, version uint64) { +func WriteDatabaseVersion(db ethdb.KeyValueWriter, version uint64) { enc, err := rlp.EncodeToBytes(version) if err != nil { log.Crit("Failed to encode database version", "err", err) @@ -53,7 +53,7 @@ func WriteDatabaseVersion(db ethdb.Writer, version uint64) { } // ReadChainConfig retrieves the consensus settings based on the given genesis hash. -func ReadChainConfig(db ethdb.Reader, hash common.Hash) *params.ChainConfig { +func ReadChainConfig(db ethdb.KeyValueReader, hash common.Hash) *params.ChainConfig { data, _ := db.Get(configKey(hash)) if len(data) == 0 { return nil @@ -67,7 +67,7 @@ func ReadChainConfig(db ethdb.Reader, hash common.Hash) *params.ChainConfig { } // WriteChainConfig writes the chain config settings to the database. -func WriteChainConfig(db ethdb.Writer, hash common.Hash, cfg *params.ChainConfig) { +func WriteChainConfig(db ethdb.KeyValueWriter, hash common.Hash, cfg *params.ChainConfig) { if cfg == nil { return } @@ -81,13 +81,13 @@ func WriteChainConfig(db ethdb.Writer, hash common.Hash, cfg *params.ChainConfig } // ReadPreimage retrieves a single preimage of the provided hash. -func ReadPreimage(db ethdb.Reader, hash common.Hash) []byte { +func ReadPreimage(db ethdb.KeyValueReader, hash common.Hash) []byte { data, _ := db.Get(preimageKey(hash)) return data } // WritePreimages writes the provided set of preimages to the database. -func WritePreimages(db ethdb.Writer, preimages map[common.Hash][]byte) { +func WritePreimages(db ethdb.KeyValueWriter, preimages map[common.Hash][]byte) { for hash, preimage := range preimages { if err := db.Put(preimageKey(hash), preimage); err != nil { log.Crit("Failed to store trie preimage", "err", err) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index cd1048cbc5..5a3c7f94b5 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -24,7 +24,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb/memorydb" ) -// freezerdb is a databse wrapper that enabled freezer data retrievals. +// freezerdb is a database wrapper that enabled freezer data retrievals. type freezerdb struct { ethdb.KeyValueStore ethdb.AncientStore @@ -51,9 +51,34 @@ type nofreezedb struct { ethdb.KeyValueStore } -// Frozen returns nil as we don't have a backing chain freezer. +// HasAncient returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) HasAncient(kind string, number uint64) (bool, error) { + return false, errNotSupported +} + +// Ancient returns an error as we don't have a backing chain freezer. func (db *nofreezedb) Ancient(kind string, number uint64) ([]byte, error) { - return nil, errOutOfBounds + return nil, errNotSupported +} + +// Ancients returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) Ancients() (uint64, error) { + return 0, errNotSupported +} + +// AppendAncient returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { + return errNotSupported +} + +// TruncateAncients returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) TruncateAncients(items uint64) error { + return errNotSupported +} + +// Sync returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) Sync() error { + return errNotSupported } // NewDatabase creates a high level database on top of a given key-value data diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 07df4c7599..84426d8ae9 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -31,9 +31,15 @@ import ( "github.com/prometheus/tsdb/fileutil" ) -// errUnknownTable is returned if the user attempts to read from a table that is -// not tracked by the freezer. -var errUnknownTable = errors.New("unknown table") +var ( + // errUnknownTable is returned if the user attempts to read from a table that is + // not tracked by the freezer. + errUnknownTable = errors.New("unknown table") + + // errOutOrderInsertion is returned if the user attempts to inject out-of-order + // binary blobs into the freezer. + errOutOrderInsertion = errors.New("the append operation is out-order") +) const ( // freezerRecheckInterval is the frequency to check the key-value database for @@ -44,7 +50,7 @@ const ( // freezerBlockGraduation is the number of confirmations a block must achieve // before it becomes elligible for chain freezing. This must exceed any chain // reorg depth, since the freezer also deletes all block siblings. - freezerBlockGraduation = 60000 + freezerBlockGraduation = 90000 // freezerBatchLimit is the maximum number of blocks to freeze in one batch // before doing an fsync and deleting it from the key-value store. @@ -72,7 +78,9 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) ) - lock, _, err := fileutil.Flock(filepath.Join(datadir, "LOCK")) + // Leveldb uses LOCK as the filelock filename. To prevent the + // name collision, we use FLOCK as the lock name. + lock, _, err := fileutil.Flock(filepath.Join(datadir, "FLOCK")) if err != nil { return nil, err } @@ -81,7 +89,7 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { tables: make(map[string]*freezerTable), instanceLock: lock, } - for _, name := range []string{"hashes", "headers", "bodies", "receipts", "diffs"} { + for _, name := range []string{freezerHashTable, freezerHeaderTable, freezerBodiesTable, freezerReceiptTable, freezerDifficultyTable} { table, err := newTable(datadir, name, readMeter, writeMeter) if err != nil { for _, table := range freezer.tables { @@ -92,21 +100,12 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { } freezer.tables[name] = table } - // Truncate all data tables to the same length - freezer.frozen = math.MaxUint64 - for _, table := range freezer.tables { - if freezer.frozen > table.items { - freezer.frozen = table.items - } - } - for _, table := range freezer.tables { - if err := table.truncate(freezer.frozen); err != nil { - for _, table := range freezer.tables { - table.Close() - } - lock.Release() - return nil, err + if err := freezer.repair(); err != nil { + for _, table := range freezer.tables { + table.Close() } + lock.Release() + return nil, err } return freezer, nil } @@ -128,8 +127,91 @@ func (f *freezer) Close() error { return nil } +// HasAncient returns an indicator whether the specified ancient data exists +// in the freezer. +func (f *freezer) HasAncient(kind string, number uint64) (bool, error) { + if table := f.tables[kind]; table != nil { + return table.has(number), nil + } + return false, nil +} + +// Ancient retrieves an ancient binary blob from the append-only immutable files. +func (f *freezer) Ancient(kind string, number uint64) ([]byte, error) { + if table := f.tables[kind]; table != nil { + return table.Retrieve(number) + } + return nil, errUnknownTable +} + +// Ancients returns the length of the frozen items. +func (f *freezer) Ancients() (uint64, error) { + return atomic.LoadUint64(&f.frozen), nil +} + +// AppendAncient injects all binary blobs belong to block at the end of the +// append-only immutable table files. +// +// Notably, this function is lock free but kind of thread-safe. All out-of-order +// injection will be rejected. But if two injections with same number happen at +// the same time, we can get into the trouble. +func (f *freezer) AppendAncient(number uint64, hash, header, body, receipts, td []byte) (err error) { + // Ensure the binary blobs we are appending is continuous with freezer. + if atomic.LoadUint64(&f.frozen) != number { + return errOutOrderInsertion + } + // Rollback all inserted data if any insertion below failed to ensure + // the tables won't out of sync. + defer func() { + if err != nil { + rerr := f.repair() + if rerr != nil { + log.Crit("Failed to repair freezer", "err", rerr) + } + log.Info("Append ancient failed", "number", number, "err", err) + } + }() + // Inject all the components into the relevant data tables + if err := f.tables[freezerHashTable].Append(f.frozen, hash[:]); err != nil { + log.Error("Failed to append ancient hash", "number", f.frozen, "hash", hash, "err", err) + return err + } + if err := f.tables[freezerHeaderTable].Append(f.frozen, header); err != nil { + log.Error("Failed to append ancient header", "number", f.frozen, "hash", hash, "err", err) + return err + } + if err := f.tables[freezerBodiesTable].Append(f.frozen, body); err != nil { + log.Error("Failed to append ancient body", "number", f.frozen, "hash", hash, "err", err) + return err + } + if err := f.tables[freezerReceiptTable].Append(f.frozen, receipts); err != nil { + log.Error("Failed to append ancient receipts", "number", f.frozen, "hash", hash, "err", err) + return err + } + if err := f.tables[freezerDifficultyTable].Append(f.frozen, td); err != nil { + log.Error("Failed to append ancient difficulty", "number", f.frozen, "hash", hash, "err", err) + return err + } + atomic.AddUint64(&f.frozen, 1) // Only modify atomically + return nil +} + +// Truncate discards any recent data above the provided threshold number. +func (f *freezer) TruncateAncients(items uint64) error { + if atomic.LoadUint64(&f.frozen) <= items { + return nil + } + for _, table := range f.tables { + if err := table.truncate(items); err != nil { + return err + } + } + atomic.StoreUint64(&f.frozen, items) + return nil +} + // sync flushes all data tables to disk. -func (f *freezer) sync() error { +func (f *freezer) Sync() error { var errs []error for _, table := range f.tables { if err := table.Sync(); err != nil { @@ -142,14 +224,6 @@ func (f *freezer) sync() error { return nil } -// Ancient retrieves an ancient binary blob from the append-only immutable files. -func (f *freezer) Ancient(kind string, number uint64) ([]byte, error) { - if table := f.tables[kind]; table != nil { - return table.Retrieve(number) - } - return nil, errUnknownTable -} - // freeze is a background thread that periodically checks the blockchain for any // import progress and moves ancient data from the fast database into the freezer. // @@ -159,25 +233,22 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) { nfdb := &nofreezedb{KeyValueStore: db} for { - // Retrieve the freezing threshold. In theory we're interested only in full - // blocks post-sync, but that would keep the live database enormous during - // dast sync. By picking the fast block, we still get to deep freeze all the - // final immutable data without having to wait for sync to finish. - hash := ReadHeadFastBlockHash(nfdb) + // Retrieve the freezing threshold. + hash := ReadHeadBlockHash(nfdb) if hash == (common.Hash{}) { - log.Debug("Current fast block hash unavailable") // new chain, empty database + log.Debug("Current full block hash unavailable") // new chain, empty database time.Sleep(freezerRecheckInterval) continue } number := ReadHeaderNumber(nfdb, hash) switch { case number == nil: - log.Error("Current fast block number unavailable", "hash", hash) + log.Error("Current full block number unavailable", "hash", hash) time.Sleep(freezerRecheckInterval) continue case *number < freezerBlockGraduation: - log.Debug("Current fast block not old enough", "number", *number, "hash", hash, "delay", freezerBlockGraduation) + log.Debug("Current full block not old enough", "number", *number, "hash", hash, "delay", freezerBlockGraduation) time.Sleep(freezerRecheckInterval) continue @@ -188,7 +259,7 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) { } head := ReadHeader(nfdb, hash, *number) if head == nil { - log.Error("Current fast block unavailable", "number", *number, "hash", hash) + log.Error("Current full block unavailable", "number", *number, "hash", hash) time.Sleep(freezerRecheckInterval) continue } @@ -229,48 +300,35 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) { log.Error("Total difficulty missing, can't freeze", "number", f.frozen, "hash", hash) break } - // Inject all the components into the relevant data tables - if err := f.tables["hashes"].Append(f.frozen, hash[:]); err != nil { - log.Error("Failed to deep freeze hash", "number", f.frozen, "hash", hash, "err", err) - break - } - if err := f.tables["headers"].Append(f.frozen, header); err != nil { - log.Error("Failed to deep freeze header", "number", f.frozen, "hash", hash, "err", err) - break - } - if err := f.tables["bodies"].Append(f.frozen, body); err != nil { - log.Error("Failed to deep freeze body", "number", f.frozen, "hash", hash, "err", err) - break - } - if err := f.tables["receipts"].Append(f.frozen, receipts); err != nil { - log.Error("Failed to deep freeze receipts", "number", f.frozen, "hash", hash, "err", err) - break - } - if err := f.tables["diffs"].Append(f.frozen, td); err != nil { - log.Error("Failed to deep freeze difficulty", "number", f.frozen, "hash", hash, "err", err) - break - } log.Trace("Deep froze ancient block", "number", f.frozen, "hash", hash) - atomic.AddUint64(&f.frozen, 1) // Only modify atomically + // Inject all the components into the relevant data tables + if err := f.AppendAncient(f.frozen, hash[:], header, body, receipts, td); err != nil { + break + } ancients = append(ancients, hash) } // Batch of blocks have been frozen, flush them before wiping from leveldb - if err := f.sync(); err != nil { + if err := f.Sync(); err != nil { log.Crit("Failed to flush frozen tables", "err", err) } // Wipe out all data from the active database batch := db.NewBatch() + for i := 0; i < len(ancients); i++ { + DeleteBlockWithoutNumber(batch, ancients[i], first+uint64(i)) + DeleteCanonicalHash(batch, first+uint64(i)) + } + if err := batch.Write(); err != nil { + log.Crit("Failed to delete frozen canonical blocks", "err", err) + } + batch.Reset() + // Wipe out side chain also. for number := first; number < f.frozen; number++ { - for _, hash := range readAllHashes(db, number) { - if hash == ancients[number-first] { - deleteBlockWithoutNumber(batch, hash, number) - } else { - DeleteBlock(batch, hash, number) - } + for _, hash := range ReadAllHashes(db, number) { + DeleteBlock(batch, hash, number) } } if err := batch.Write(); err != nil { - log.Crit("Failed to delete frozen items", "err", err) + log.Crit("Failed to delete frozen side blocks", "err", err) } // Log something friendly for the user context := []interface{}{ @@ -287,3 +345,21 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) { } } } + +// repair truncates all data tables to the same length. +func (f *freezer) repair() error { + min := uint64(math.MaxUint64) + for _, table := range f.tables { + items := atomic.LoadUint64(&table.items) + if min > items { + min = items + } + } + for _, table := range f.tables { + if err := table.truncate(min); err != nil { + return err + } + } + atomic.StoreUint64(&f.frozen, min) + return nil +} diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index 313ac8b78c..8e117301b2 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -39,6 +39,9 @@ var ( // errOutOfBounds is returned if the item requested is not contained within the // freezer table. errOutOfBounds = errors.New("out of bounds") + + // errNotSupported is returned if the database doesn't support the required operation. + errNotSupported = errors.New("this operation is not supported") ) // indexEntry contains the number/id of the file that the data resides in, aswell as the @@ -451,7 +454,6 @@ func (t *freezerTable) getBounds(item uint64) (uint32, uint32, uint32, error) { // Retrieve looks up the data offset of an item with the given number and retrieves // the raw binary blob from the data file. func (t *freezerTable) Retrieve(item uint64) ([]byte, error) { - // Ensure the table and the item is accessible if t.index == nil || t.head == nil { return nil, errClosed @@ -483,6 +485,12 @@ func (t *freezerTable) Retrieve(item uint64) ([]byte, error) { return snappy.Decode(nil, blob) } +// has returns an indicator whether the specified number data +// exists in the freezer table. +func (t *freezerTable) has(number uint64) bool { + return atomic.LoadUint64(&t.items) > number +} + // Sync pushes any pending data from memory out to disk. This is an expensive // operation, so use it with care. func (t *freezerTable) Sync() error { diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 62b60e2f3b..ebca172524 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -63,6 +63,23 @@ var ( preimageHitCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil) ) +const ( + // freezerHeaderTable indicates the name of the freezer header table. + freezerHeaderTable = "headers" + + // freezerHashTable indicates the name of the freezer canonical hash table. + freezerHashTable = "hashes" + + // freezerBodiesTable indicates the name of the freezer block body table. + freezerBodiesTable = "bodies" + + // freezerReceiptTable indicates the name of the freezer receipts table. + freezerReceiptTable = "receipts" + + // freezerDifficultyTable indicates the name of the freezer total difficulty table. + freezerDifficultyTable = "diffs" +) + // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary // fields. type LegacyTxLookupEntry struct { diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 0b5e08b207..1246789595 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -50,12 +50,42 @@ func (t *table) Get(key []byte) ([]byte, error) { return t.db.Get(append([]byte(t.prefix), key...)) } +// HasAncient is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) HasAncient(kind string, number uint64) (bool, error) { + return t.db.HasAncient(kind, number) +} + // Ancient is a noop passthrough that just forwards the request to the underlying // database. func (t *table) Ancient(kind string, number uint64) ([]byte, error) { return t.db.Ancient(kind, number) } +// Ancients is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) Ancients() (uint64, error) { + return t.db.Ancients() +} + +// AppendAncient is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { + return t.db.AppendAncient(number, hash, header, body, receipts, td) +} + +// TruncateAncients is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) TruncateAncients(items uint64) error { + return t.db.TruncateAncients(items) +} + +// Sync is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) Sync() error { + return t.db.Sync() +} + // Put inserts the given value into the database at a prefixed version of the // provided key. func (t *table) Put(key []byte, value []byte) error { @@ -163,6 +193,6 @@ func (b *tableBatch) Reset() { } // Replay replays the batch contents. -func (b *tableBatch) Replay(w ethdb.Writer) error { +func (b *tableBatch) Replay(w ethdb.KeyValueWriter) error { return b.batch.Replay(w) } diff --git a/core/state/database.go b/core/state/database.go index 8798b73806..ecc2c134da 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -93,7 +93,7 @@ type Trie interface { // If the trie does not contain a value for key, the returned proof contains all // nodes of the longest existing prefix of the key (at least the root), ending // with the node that proves the absence of the key. - Prove(key []byte, fromLevel uint, proofDb ethdb.Writer) error + Prove(key []byte, fromLevel uint, proofDb ethdb.KeyValueWriter) error } // NewDatabase creates a backing store for state. The returned database is safe for diff --git a/core/state/sync.go b/core/state/sync.go index e4a08d2938..ef79305273 100644 --- a/core/state/sync.go +++ b/core/state/sync.go @@ -26,7 +26,7 @@ import ( ) // NewStateSync create a new state trie download scheduler. -func NewStateSync(root common.Hash, database ethdb.Reader, bloom *trie.SyncBloom) *trie.Sync { +func NewStateSync(root common.Hash, database ethdb.KeyValueReader, bloom *trie.SyncBloom) *trie.Sync { var syncer *trie.Sync callback := func(leaf []byte, parent common.Hash) error { var obj Account diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 0e19fe9e65..79107c8d10 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -129,6 +129,7 @@ type Downloader struct { synchronising int32 notified int32 committed int32 + ancientLimit uint64 // The maximum block number which can be regarded as ancient data. // Channels headerCh chan dataPack // [eth/62] Channel receiving inbound block headers @@ -206,7 +207,7 @@ type BlockChain interface { InsertChain(types.Blocks) (int, error) // InsertReceiptChain inserts a batch of receipts into the local chain. - InsertReceiptChain(types.Blocks, []types.Receipts) (int, error) + InsertReceiptChain(types.Blocks, []types.Receipts, uint64) (int, error) } // New creates a new downloader to fetch hashes and blocks from remote peers. @@ -475,12 +476,49 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.I if d.mode == FastSync && pivot != 0 { d.committed = 0 } + if d.mode == FastSync { + // Set the ancient data limitation. + // If we are running fast sync, all block data not greater than ancientLimit will + // be written to the ancient store. Otherwise, block data will be written to active + // database and then wait freezer to migrate. + // + // If there is checkpoint available, then calculate the ancientLimit through + // checkpoint. Otherwise calculate the ancient limit through the advertised + // height by remote peer. + // + // The reason for picking checkpoint first is: there exists an attack vector + // for height that: a malicious peer can give us a fake(very high) height, + // so that the ancient limit is also very high. And then the peer start to + // feed us valid blocks until head. All of these blocks might be written into + // the ancient store, the safe region for freezer is not enough. + if d.checkpoint != 0 && d.checkpoint > MaxForkAncestry+1 { + d.ancientLimit = height - MaxForkAncestry - 1 + } else if height > MaxForkAncestry+1 { + d.ancientLimit = height - MaxForkAncestry - 1 + } + frozen, _ := d.stateDB.Ancients() // Ignore the error here since light client can also hit here. + // If a part of blockchain data has already been written into active store, + // disable the ancient style insertion explicitly. + if origin >= frozen && frozen != 0 { + d.ancientLimit = 0 + log.Info("Disabling direct-ancient mode", "origin", origin, "ancient", frozen-1) + } else if d.ancientLimit > 0 { + log.Debug("Enabling direct-ancient mode", "ancient", d.ancientLimit) + } + // Rewind the ancient store and blockchain if reorg happens. + if origin+1 < frozen { + var hashes []common.Hash + for i := origin + 1; i < d.lightchain.CurrentHeader().Number.Uint64(); i++ { + hashes = append(hashes, rawdb.ReadCanonicalHash(d.stateDB, i)) + } + d.lightchain.Rollback(hashes) + } + } // Initiate the sync using a concurrent header and content retrieval algorithm d.queue.Prepare(origin+1, d.mode) if d.syncInitHook != nil { d.syncInitHook(origin, height) } - fetchers := []func() error{ func() error { return d.fetchHeaders(p, origin+1, pivot) }, // Headers are always retrieved func() error { return d.fetchBodies(origin + 1) }, // Bodies are retrieved during normal and fast sync @@ -544,6 +582,9 @@ func (d *Downloader) cancel() { func (d *Downloader) Cancel() { d.cancel() d.cancelWg.Wait() + + d.ancientLimit = 0 + log.Debug("Reset ancient limit to zero") } // Terminate interrupts the downloader, canceling all pending operations. @@ -1315,7 +1356,7 @@ func (d *Downloader) fetchParts(errCancel error, deliveryCh chan dataPack, deliv // queue until the stream ends or a failure occurs. func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) error { // Keep a count of uncertain headers to roll back - rollback := []*types.Header{} + var rollback []*types.Header defer func() { if len(rollback) > 0 { // Flatten the headers and roll them back @@ -1409,11 +1450,10 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er limit = len(headers) } chunk := headers[:limit] - // In case of header only syncing, validate the chunk immediately if d.mode == FastSync || d.mode == LightSync { // Collect the yet unknown headers to mark them as uncertain - unknown := make([]*types.Header, 0, len(headers)) + unknown := make([]*types.Header, 0, len(chunk)) for _, header := range chunk { if !d.lightchain.HasHeader(header.Hash(), header.Number.Uint64()) { unknown = append(unknown, header) @@ -1663,7 +1703,7 @@ func (d *Downloader) commitFastSyncData(results []*fetchResult, stateSync *state blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles) receipts[i] = result.Receipts } - if index, err := d.blockchain.InsertReceiptChain(blocks, receipts); err != nil { + if index, err := d.blockchain.InsertReceiptChain(blocks, receipts, d.ancientLimit); err != nil { log.Debug("Downloaded item processing failed", "number", results[index].Header.Number, "hash", results[index].Header.Hash(), "err", err) return errInvalidChain } @@ -1675,7 +1715,7 @@ func (d *Downloader) commitPivotBlock(result *fetchResult) error { log.Debug("Committing fast sync pivot as new head", "number", block.Number(), "hash", block.Hash()) // Commit the pivot block as the new head, will require full sync from here on - if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []types.Receipts{result.Receipts}); err != nil { + if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []types.Receipts{result.Receipts}, d.ancientLimit); err != nil { return err } if err := d.blockchain.FastSyncCommitHead(block.Hash()); err != nil { diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index ac7f48abdb..659aee0881 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -57,6 +57,11 @@ type downloadTester struct { ownReceipts map[common.Hash]types.Receipts // Receipts belonging to the tester ownChainTd map[common.Hash]*big.Int // Total difficulties of the blocks in the local chain + ancientHeaders map[common.Hash]*types.Header // Ancient headers belonging to the tester + ancientBlocks map[common.Hash]*types.Block // Ancient blocks belonging to the tester + ancientReceipts map[common.Hash]types.Receipts // Ancient receipts belonging to the tester + ancientChainTd map[common.Hash]*big.Int // Ancient total difficulties of the blocks in the local chain + lock sync.RWMutex } @@ -71,6 +76,12 @@ func newTester() *downloadTester { ownBlocks: map[common.Hash]*types.Block{testGenesis.Hash(): testGenesis}, ownReceipts: map[common.Hash]types.Receipts{testGenesis.Hash(): nil}, ownChainTd: map[common.Hash]*big.Int{testGenesis.Hash(): testGenesis.Difficulty()}, + + // Initialize ancient store with test genesis block + ancientHeaders: map[common.Hash]*types.Header{testGenesis.Hash(): testGenesis.Header()}, + ancientBlocks: map[common.Hash]*types.Block{testGenesis.Hash(): testGenesis}, + ancientReceipts: map[common.Hash]types.Receipts{testGenesis.Hash(): nil}, + ancientChainTd: map[common.Hash]*big.Int{testGenesis.Hash(): testGenesis.Difficulty()}, } tester.stateDb = rawdb.NewMemoryDatabase() tester.stateDb.Put(testGenesis.Root().Bytes(), []byte{0x00}) @@ -122,6 +133,9 @@ func (dl *downloadTester) HasFastBlock(hash common.Hash, number uint64) bool { dl.lock.RLock() defer dl.lock.RUnlock() + if _, ok := dl.ancientReceipts[hash]; ok { + return true + } _, ok := dl.ownReceipts[hash] return ok } @@ -131,6 +145,10 @@ func (dl *downloadTester) GetHeaderByHash(hash common.Hash) *types.Header { dl.lock.RLock() defer dl.lock.RUnlock() + header := dl.ancientHeaders[hash] + if header != nil { + return header + } return dl.ownHeaders[hash] } @@ -139,6 +157,10 @@ func (dl *downloadTester) GetBlockByHash(hash common.Hash) *types.Block { dl.lock.RLock() defer dl.lock.RUnlock() + block := dl.ancientBlocks[hash] + if block != nil { + return block + } return dl.ownBlocks[hash] } @@ -148,6 +170,9 @@ func (dl *downloadTester) CurrentHeader() *types.Header { defer dl.lock.RUnlock() for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if header := dl.ancientHeaders[dl.ownHashes[i]]; header != nil { + return header + } if header := dl.ownHeaders[dl.ownHashes[i]]; header != nil { return header } @@ -161,6 +186,12 @@ func (dl *downloadTester) CurrentBlock() *types.Block { defer dl.lock.RUnlock() for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if block := dl.ancientBlocks[dl.ownHashes[i]]; block != nil { + if _, err := dl.stateDb.Get(block.Root().Bytes()); err == nil { + return block + } + return block + } if block := dl.ownBlocks[dl.ownHashes[i]]; block != nil { if _, err := dl.stateDb.Get(block.Root().Bytes()); err == nil { return block @@ -176,6 +207,9 @@ func (dl *downloadTester) CurrentFastBlock() *types.Block { defer dl.lock.RUnlock() for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if block := dl.ancientBlocks[dl.ownHashes[i]]; block != nil { + return block + } if block := dl.ownBlocks[dl.ownHashes[i]]; block != nil { return block } @@ -198,6 +232,9 @@ func (dl *downloadTester) GetTd(hash common.Hash, number uint64) *big.Int { dl.lock.RLock() defer dl.lock.RUnlock() + if td := dl.ancientChainTd[hash]; td != nil { + return td + } return dl.ownChainTd[hash] } @@ -254,7 +291,7 @@ func (dl *downloadTester) InsertChain(blocks types.Blocks) (i int, err error) { } // InsertReceiptChain injects a new batch of receipts into the simulated chain. -func (dl *downloadTester) InsertReceiptChain(blocks types.Blocks, receipts []types.Receipts) (i int, err error) { +func (dl *downloadTester) InsertReceiptChain(blocks types.Blocks, receipts []types.Receipts, ancientLimit uint64) (i int, err error) { dl.lock.Lock() defer dl.lock.Unlock() @@ -262,11 +299,25 @@ func (dl *downloadTester) InsertReceiptChain(blocks types.Blocks, receipts []typ if _, ok := dl.ownHeaders[blocks[i].Hash()]; !ok { return i, errors.New("unknown owner") } - if _, ok := dl.ownBlocks[blocks[i].ParentHash()]; !ok { - return i, errors.New("unknown parent") + if _, ok := dl.ancientBlocks[blocks[i].ParentHash()]; !ok { + if _, ok := dl.ownBlocks[blocks[i].ParentHash()]; !ok { + return i, errors.New("unknown parent") + } + } + if blocks[i].NumberU64() <= ancientLimit { + dl.ancientBlocks[blocks[i].Hash()] = blocks[i] + dl.ancientReceipts[blocks[i].Hash()] = receipts[i] + + // Migrate from active db to ancient db + dl.ancientHeaders[blocks[i].Hash()] = blocks[i].Header() + dl.ancientChainTd[blocks[i].Hash()] = new(big.Int).Add(dl.ancientChainTd[blocks[i].ParentHash()], blocks[i].Difficulty()) + + delete(dl.ownHeaders, blocks[i].Hash()) + delete(dl.ownChainTd, blocks[i].Hash()) + } else { + dl.ownBlocks[blocks[i].Hash()] = blocks[i] + dl.ownReceipts[blocks[i].Hash()] = receipts[i] } - dl.ownBlocks[blocks[i].Hash()] = blocks[i] - dl.ownReceipts[blocks[i].Hash()] = receipts[i] } return len(blocks), nil } @@ -284,6 +335,11 @@ func (dl *downloadTester) Rollback(hashes []common.Hash) { delete(dl.ownHeaders, hashes[i]) delete(dl.ownReceipts, hashes[i]) delete(dl.ownBlocks, hashes[i]) + + delete(dl.ancientChainTd, hashes[i]) + delete(dl.ancientHeaders, hashes[i]) + delete(dl.ancientReceipts, hashes[i]) + delete(dl.ancientBlocks, hashes[i]) } } @@ -411,13 +467,13 @@ func assertOwnForkedChain(t *testing.T, tester *downloadTester, common int, leng if tester.downloader.mode == LightSync { blocks, receipts = 1, 1 } - if hs := len(tester.ownHeaders); hs != headers { + if hs := len(tester.ownHeaders) + len(tester.ancientHeaders) - 1; hs != headers { t.Fatalf("synchronised headers mismatch: have %v, want %v", hs, headers) } - if bs := len(tester.ownBlocks); bs != blocks { + if bs := len(tester.ownBlocks) + len(tester.ancientBlocks) - 1; bs != blocks { t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, blocks) } - if rs := len(tester.ownReceipts); rs != receipts { + if rs := len(tester.ownReceipts) + len(tester.ancientReceipts) - 1; rs != receipts { t.Fatalf("synchronised receipts mismatch: have %v, want %v", rs, receipts) } } diff --git a/ethdb/batch.go b/ethdb/batch.go index a9c4063546..e261415bff 100644 --- a/ethdb/batch.go +++ b/ethdb/batch.go @@ -23,7 +23,7 @@ const IdealBatchSize = 100 * 1024 // Batch is a write-only database that commits changes to its host database // when Write is called. A batch cannot be used concurrently. type Batch interface { - Writer + KeyValueWriter // ValueSize retrieves the amount of data queued up for writing. ValueSize() int @@ -35,7 +35,7 @@ type Batch interface { Reset() // Replay replays the batch contents. - Replay(w Writer) error + Replay(w KeyValueWriter) error } // Batcher wraps the NewBatch method of a backing data store. diff --git a/ethdb/database.go b/ethdb/database.go index 01483f3d4d..e3eff32dba 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -19,8 +19,8 @@ package ethdb import "io" -// Reader wraps the Has and Get method of a backing data store. -type Reader interface { +// KeyValueReader wraps the Has and Get method of a backing data store. +type KeyValueReader interface { // Has retrieves if a key is present in the key-value data store. Has(key []byte) (bool, error) @@ -28,8 +28,8 @@ type Reader interface { Get(key []byte) ([]byte, error) } -// Writer wraps the Put method of a backing data store. -type Writer interface { +// KeyValueWriter wraps the Put method of a backing data store. +type KeyValueWriter interface { // Put inserts the given value into the key-value data store. Put(key []byte, value []byte) error @@ -58,8 +58,8 @@ type Compacter interface { // KeyValueStore contains all the methods required to allow handling different // key-value data stores backing the high level database. type KeyValueStore interface { - Reader - Writer + KeyValueReader + KeyValueWriter Batcher Iteratee Stater @@ -67,30 +67,58 @@ type KeyValueStore interface { io.Closer } -// Ancienter wraps the Ancient method for a backing immutable chain data store. -type Ancienter interface { +// AncientReader contains the methods required to read from immutable ancient data. +type AncientReader interface { + // HasAncient returns an indicator whether the specified data exists in the + // ancient store. + HasAncient(kind string, number uint64) (bool, error) + // Ancient retrieves an ancient binary blob from the append-only immutable files. Ancient(kind string, number uint64) ([]byte, error) + + // Ancients returns the ancient store length + Ancients() (uint64, error) } -// AncientReader contains the methods required to access both key-value as well as +// AncientWriter contains the methods required to write to immutable ancient data. +type AncientWriter interface { + // AppendAncient injects all binary blobs belong to block at the end of the + // append-only immutable table files. + AppendAncient(number uint64, hash, header, body, receipt, td []byte) error + + // TruncateAncients discards all but the first n ancient data from the ancient store. + TruncateAncients(n uint64) error + + // Sync flushes all in-memory ancient store data to disk. + Sync() error +} + +// Reader contains the methods required to read data from both key-value as well as // immutable ancient data. -type AncientReader interface { - Reader - Ancienter +type Reader interface { + KeyValueReader + AncientReader +} + +// Writer contains the methods required to write data to both key-value as well as +// immutable ancient data. +type Writer interface { + KeyValueWriter + AncientWriter } // AncientStore contains all the methods required to allow handling different // ancient data stores backing immutable chain data store. type AncientStore interface { - Ancienter + AncientReader + AncientWriter io.Closer } // Database contains all the methods required by the high level database to not // only access the key-value data store but also the chain freezer. type Database interface { - AncientReader + Reader Writer Batcher Iteratee diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index 8eabee50e2..f74e94d92a 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -425,13 +425,13 @@ func (b *batch) Reset() { } // Replay replays the batch contents. -func (b *batch) Replay(w ethdb.Writer) error { +func (b *batch) Replay(w ethdb.KeyValueWriter) error { return b.b.Replay(&replayer{writer: w}) } // replayer is a small wrapper to implement the correct replay methods. type replayer struct { - writer ethdb.Writer + writer ethdb.KeyValueWriter failure error } diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index cb8b27f3b3..caa9b02a13 100644 --- a/ethdb/memorydb/memorydb.go +++ b/ethdb/memorydb/memorydb.go @@ -270,7 +270,7 @@ func (b *batch) Reset() { } // Replay replays the batch contents. -func (b *batch) Replay(w ethdb.Writer) error { +func (b *batch) Replay(w ethdb.KeyValueWriter) error { for _, keyvalue := range b.writes { if keyvalue.delete { if err := w.Delete(keyvalue.key); err != nil { diff --git a/les/odr_requests.go b/les/odr_requests.go index 328750d7ab..89c6091777 100644 --- a/les/odr_requests.go +++ b/les/odr_requests.go @@ -514,7 +514,7 @@ func (r *TxStatusRequest) Validate(db ethdb.Database, msg *Msg) error { // readTraceDB stores the keys of database reads. We use this to check that received node // sets contain only the trie nodes necessary to make proofs pass. type readTraceDB struct { - db ethdb.Reader + db ethdb.KeyValueReader reads map[string]struct{} } diff --git a/light/lightchain.go b/light/lightchain.go index 86836dfb5f..f0beec47bb 100644 --- a/light/lightchain.go +++ b/light/lightchain.go @@ -165,12 +165,12 @@ func (lc *LightChain) loadLastState() error { // SetHead rewinds the local chain to a new head. Everything above the new // head will be deleted and the new one set. -func (lc *LightChain) SetHead(head uint64) { +func (lc *LightChain) SetHead(head uint64) error { lc.chainmu.Lock() defer lc.chainmu.Unlock() - lc.hc.SetHead(head, nil) - lc.loadLastState() + lc.hc.SetHead(head, nil, nil) + return lc.loadLastState() } // GasLimit returns the gas limit of the current HEAD block. diff --git a/light/nodeset.go b/light/nodeset.go index a8bf4f6c65..3662596785 100644 --- a/light/nodeset.go +++ b/light/nodeset.go @@ -115,7 +115,7 @@ func (db *NodeSet) NodeList() NodeList { } // Store writes the contents of the set to the given database -func (db *NodeSet) Store(target ethdb.Writer) { +func (db *NodeSet) Store(target ethdb.KeyValueWriter) { db.lock.RLock() defer db.lock.RUnlock() @@ -124,11 +124,11 @@ func (db *NodeSet) Store(target ethdb.Writer) { } } -// NodeList stores an ordered list of trie nodes. It implements ethdb.Writer. +// NodeList stores an ordered list of trie nodes. It implements ethdb.KeyValueWriter. type NodeList []rlp.RawValue // Store writes the contents of the list to the given database -func (n NodeList) Store(db ethdb.Writer) { +func (n NodeList) Store(db ethdb.KeyValueWriter) { for _, node := range n { db.Put(crypto.Keccak256(node), node) } diff --git a/light/trie.go b/light/trie.go index 27abb1dc28..e512bf6f95 100644 --- a/light/trie.go +++ b/light/trie.go @@ -141,7 +141,7 @@ func (t *odrTrie) GetKey(sha []byte) []byte { return nil } -func (t *odrTrie) Prove(key []byte, fromLevel uint, proofDb ethdb.Writer) error { +func (t *odrTrie) Prove(key []byte, fromLevel uint, proofDb ethdb.KeyValueWriter) error { return errors.New("not implemented, needs client/server interface split") } diff --git a/trie/database.go b/trie/database.go index 49a696befb..d8a0fa9c53 100644 --- a/trie/database.go +++ b/trie/database.go @@ -321,7 +321,7 @@ func NewDatabaseWithCache(diskdb ethdb.KeyValueStore, cache int) *Database { } // DiskDB retrieves the persistent storage backing the trie database. -func (db *Database) DiskDB() ethdb.Reader { +func (db *Database) DiskDB() ethdb.KeyValueReader { return db.diskdb } diff --git a/trie/proof.go b/trie/proof.go index 3c4b8d653d..9985e730dd 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -33,7 +33,7 @@ import ( // If the trie does not contain a value for key, the returned proof contains all // nodes of the longest existing prefix of the key (at least the root node), ending // with the node that proves the absence of the key. -func (t *Trie) Prove(key []byte, fromLevel uint, proofDb ethdb.Writer) error { +func (t *Trie) Prove(key []byte, fromLevel uint, proofDb ethdb.KeyValueWriter) error { // Collect all nodes on the path to key. key = keybytesToHex(key) var nodes []node @@ -96,16 +96,14 @@ func (t *Trie) Prove(key []byte, fromLevel uint, proofDb ethdb.Writer) error { // If the trie does not contain a value for key, the returned proof contains all // nodes of the longest existing prefix of the key (at least the root node), ending // with the node that proves the absence of the key. -func (t *SecureTrie) Prove(key []byte, fromLevel uint, proofDb ethdb.Writer) error { +func (t *SecureTrie) Prove(key []byte, fromLevel uint, proofDb ethdb.KeyValueWriter) error { return t.trie.Prove(key, fromLevel, proofDb) } // VerifyProof checks merkle proofs. The given proof must contain the value for // key in a trie with the given root hash. VerifyProof returns an error if the // proof contains invalid trie nodes or the wrong value. -// -// Note, the method assumes that all key-values in proofDb satisfy key = hash(value). -func VerifyProof(rootHash common.Hash, key []byte, proofDb ethdb.Reader) (value []byte, nodes int, err error) { +func VerifyProof(rootHash common.Hash, key []byte, proofDb ethdb.KeyValueReader) (value []byte, nodes int, err error) { key = keybytesToHex(key) wantHash := rootHash for i := 0; ; i++ { diff --git a/trie/sync.go b/trie/sync.go index d9564d7831..6f40b45a1e 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -72,7 +72,7 @@ func newSyncMemBatch() *syncMemBatch { // unknown trie hashes to retrieve, accepts node data associated with said hashes // and reconstructs the trie step by step until all is done. type Sync struct { - database ethdb.Reader // Persistent database to check for existing entries + database ethdb.KeyValueReader // Persistent database to check for existing entries membatch *syncMemBatch // Memory buffer to avoid frequent database writes requests map[common.Hash]*request // Pending requests pertaining to a key hash queue *prque.Prque // Priority queue with the pending requests @@ -80,7 +80,7 @@ type Sync struct { } // NewSync creates a new trie data download scheduler. -func NewSync(root common.Hash, database ethdb.Reader, callback LeafCallback, bloom *SyncBloom) *Sync { +func NewSync(root common.Hash, database ethdb.KeyValueReader, callback LeafCallback, bloom *SyncBloom) *Sync { ts := &Sync{ database: database, membatch: newSyncMemBatch(), @@ -224,7 +224,7 @@ func (s *Sync) Process(results []SyncResult) (bool, int, error) { // Commit flushes the data stored in the internal membatch out to persistent // storage, returning the number of items written and any occurred error. -func (s *Sync) Commit(dbw ethdb.Writer) (int, error) { +func (s *Sync) Commit(dbw ethdb.KeyValueWriter) (int, error) { // Dump the membatch into a database dbw for i, key := range s.membatch.order { if err := dbw.Put(key[:], s.membatch.batch[key]); err != nil { From 331de17e4d773803c0d507bd574361f777acdf57 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Sun, 14 Apr 2019 21:25:32 +0200 Subject: [PATCH 22/34] core/rawdb: support starting offset for future deletion --- core/rawdb/freezer_table.go | 65 ++++++++++++-- core/rawdb/freezer_table_test.go | 140 ++++++++++++++++++++++++++----- 2 files changed, 175 insertions(+), 30 deletions(-) diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index 8e117301b2..93636a5ba7 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -81,9 +81,14 @@ type freezerTable struct { head *os.File // File descriptor for the data head of the table files map[uint32]*os.File // open files headId uint32 // number of the currently active head file + tailId uint32 // number of the earliest file index *os.File // File descriptor for the indexEntry file of the table - items uint64 // Number of items stored in the table + // In the case that old items are deleted (from the tail), we use itemOffset + // to count how many historic items have gone missing. + items uint64 // Number of items stored in the table (including items removed from tail) + itemOffset uint32 // Offset (number of discarded items) + headBytes uint32 // Number of bytes written to the head file readMeter metrics.Meter // Meter for measuring the effective amount of data read writeMeter metrics.Meter // Meter for measuring the effective amount of data written @@ -164,10 +169,19 @@ func (t *freezerTable) repair() error { // Open the head file var ( + firstIndex indexEntry lastIndex indexEntry contentSize int64 contentExp int64 ) + // Read index zero, determine what file is the earliest + // and what item offset to use + t.index.ReadAt(buffer, 0) + firstIndex.unmarshalBinary(buffer) + + t.tailId = firstIndex.offset + t.itemOffset = firstIndex.filenum + t.index.ReadAt(buffer, offsetsSize-indexEntrySize) lastIndex.unmarshalBinary(buffer) t.head, err = t.openFile(lastIndex.filenum, os.O_RDWR|os.O_CREATE|os.O_APPEND) @@ -225,7 +239,7 @@ func (t *freezerTable) repair() error { return err } // Update the item and byte counters and return - t.items = uint64(offsetsSize/indexEntrySize - 1) // last indexEntry points to the end of the data file + t.items = uint64(t.itemOffset) + uint64(offsetsSize/indexEntrySize-1) // last indexEntry points to the end of the data file t.headBytes = uint32(contentSize) t.headId = lastIndex.filenum @@ -245,7 +259,7 @@ func (t *freezerTable) preopen() (err error) { // The repair might have already opened (some) files t.releaseFilesAfter(0, false) // Open all except head in RDONLY - for i := uint32(0); i < t.headId; i++ { + for i := uint32(t.tailId); i < t.headId; i++ { if _, err = t.openFile(i, os.O_RDONLY); err != nil { return err } @@ -259,7 +273,8 @@ func (t *freezerTable) preopen() (err error) { func (t *freezerTable) truncate(items uint64) error { t.lock.Lock() defer t.lock.Unlock() - // If out item count is corrent, don't do anything + + // If our item count is correct, don't do anything if atomic.LoadUint64(&t.items) <= items { return nil } @@ -275,6 +290,7 @@ func (t *freezerTable) truncate(items uint64) error { } var expected indexEntry expected.unmarshalBinary(buffer) + // We might need to truncate back to older files if expected.filenum != t.headId { // If already open for reading, force-reopen for writing @@ -290,7 +306,6 @@ func (t *freezerTable) truncate(items uint64) error { t.head = newHead atomic.StoreUint32(&t.headId, expected.filenum) } - if err := t.head.Truncate(int64(expected.offset)); err != nil { return err } @@ -330,9 +345,9 @@ func (t *freezerTable) openFile(num uint32, flag int) (f *os.File, err error) { if f, exist = t.files[num]; !exist { var name string if t.noCompression { - name = fmt.Sprintf("%s.%d.rdat", t.name, num) + name = fmt.Sprintf("%s.%04d.rdat", t.name, num) } else { - name = fmt.Sprintf("%s.%d.cdat", t.name, num) + name = fmt.Sprintf("%s.%04d.cdat", t.name, num) } f, err = os.OpenFile(filepath.Join(t.path, name), flag, 0644) if err != nil { @@ -376,11 +391,13 @@ func (t *freezerTable) Append(item uint64, blob []byte) error { t.lock.RLock() // Ensure the table is still accessible if t.index == nil || t.head == nil { + t.lock.RUnlock() return errClosed } // Ensure only the next item can be written, nothing else if atomic.LoadUint64(&t.items) != item { - panic(fmt.Sprintf("appending unexpected item: want %d, have %d", t.items, item)) + t.lock.RUnlock() + return fmt.Errorf("appending unexpected item: want %d, have %d", t.items, item) } // Encode the blob and write it into the data file if !t.noCompression { @@ -461,13 +478,20 @@ func (t *freezerTable) Retrieve(item uint64) ([]byte, error) { if atomic.LoadUint64(&t.items) <= item { return nil, errOutOfBounds } + // Ensure the item was not deleted from the tail either + offset := atomic.LoadUint32(&t.itemOffset) + if uint64(offset) > item { + return nil, errOutOfBounds + } t.lock.RLock() - startOffset, endOffset, filenum, err := t.getBounds(item) + startOffset, endOffset, filenum, err := t.getBounds(item - uint64(offset)) if err != nil { + t.lock.RUnlock() return nil, err } dataFile, exist := t.files[filenum] if !exist { + t.lock.RUnlock() return nil, fmt.Errorf("missing data file %d", filenum) } // Retrieve the data itself, decompress and return @@ -499,3 +523,26 @@ func (t *freezerTable) Sync() error { } return t.head.Sync() } + +// printIndex is a debug print utility function for testing +func (t *freezerTable) printIndex() { + buf := make([]byte, indexEntrySize) + + fmt.Printf("|-----------------|\n") + fmt.Printf("| fileno | offset |\n") + fmt.Printf("|--------+--------|\n") + + for i := uint64(0); ; i++ { + if _, err := t.index.ReadAt(buf, int64(i*indexEntrySize)); err != nil { + break + } + var entry indexEntry + entry.unmarshalBinary(buf) + fmt.Printf("| %03d | %03d | \n", entry.filenum, entry.offset) + if i > 100 { + fmt.Printf(" ... \n") + break + } + } + fmt.Printf("|-----------------|\n") +} diff --git a/core/rawdb/freezer_table_test.go b/core/rawdb/freezer_table_test.go index d6ce6e93c0..9a7eec5054 100644 --- a/core/rawdb/freezer_table_test.go +++ b/core/rawdb/freezer_table_test.go @@ -19,12 +19,13 @@ package rawdb import ( "bytes" "fmt" - "github.com/ethereum/go-ethereum/metrics" "math/rand" "os" "path/filepath" "testing" "time" + + "github.com/ethereum/go-ethereum/metrics" ) func init() { @@ -32,10 +33,10 @@ func init() { } // Gets a chunk of data, filled with 'b' -func getChunk(size int, b byte) []byte { +func getChunk(size int, b int) []byte { data := make([]byte, size) for i, _ := range data { - data[i] = b + data[i] = byte(b) } return data } @@ -61,7 +62,7 @@ func TestFreezerBasics(t *testing.T) { } defer f.Close() // Write 15 bytes 255 times, results in 85 files - for x := byte(0); x < 255; x++ { + for x := 0; x < 255; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -74,7 +75,7 @@ func TestFreezerBasics(t *testing.T) { //db[1] = 010101010101010101010101010101 //db[2] = 020202020202020202020202020202 - for y := byte(0); y < 255; y++ { + for y := 0; y < 255; y++ { exp := getChunk(15, y) got, err := f.Retrieve(uint64(y)) if err != nil { @@ -84,6 +85,11 @@ func TestFreezerBasics(t *testing.T) { t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) } } + // Check that we cannot read too far + _, err = f.Retrieve(uint64(255)) + if err != errOutOfBounds { + t.Fatal(err) + } } // TestFreezerBasicsClosing tests same as TestFreezerBasics, but also closes and reopens the freezer between @@ -102,18 +108,15 @@ func TestFreezerBasicsClosing(t *testing.T) { t.Fatal(err) } // Write 15 bytes 255 times, results in 85 files - for x := byte(0); x < 255; x++ { + for x := 0; x < 255; x++ { data := getChunk(15, x) f.Append(uint64(x), data) f.Close() f, err = newCustomTable(os.TempDir(), fname, m1, m2, 50, true) - if err != nil { - t.Fatal(err) - } } defer f.Close() - for y := byte(0); y < 255; y++ { + for y := 0; y < 255; y++ { exp := getChunk(15, y) got, err := f.Retrieve(uint64(y)) if err != nil { @@ -142,7 +145,7 @@ func TestFreezerRepairDanglingHead(t *testing.T) { t.Fatal(err) } // Write 15 bytes 255 times - for x := byte(0); x < 0xff; x++ { + for x := 0; x < 255; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -190,7 +193,7 @@ func TestFreezerRepairDanglingHeadLarge(t *testing.T) { t.Fatal(err) } // Write 15 bytes 255 times - for x := byte(0); x < 0xff; x++ { + for x := 0; x < 0xff; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -223,7 +226,7 @@ func TestFreezerRepairDanglingHeadLarge(t *testing.T) { t.Errorf("Expected error for missing index entry") } // We should now be able to store items again, from item = 1 - for x := byte(1); x < 0xff; x++ { + for x := 1; x < 0xff; x++ { data := getChunk(15, ^x) f.Append(uint64(x), data) } @@ -232,7 +235,7 @@ func TestFreezerRepairDanglingHeadLarge(t *testing.T) { // And if we open it, we should now be able to read all of them (new values) { f, _ := newCustomTable(os.TempDir(), fname, rm, wm, 50, true) - for y := byte(1); y < 255; y++ { + for y := 1; y < 255; y++ { exp := getChunk(15, ^y) got, err := f.Retrieve(uint64(y)) if err != nil { @@ -257,7 +260,7 @@ func TestSnappyDetection(t *testing.T) { t.Fatal(err) } // Write 15 bytes 255 times - for x := byte(0); x < 0xff; x++ { + for x := 0; x < 0xff; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -308,7 +311,7 @@ func TestFreezerRepairDanglingIndex(t *testing.T) { t.Fatal(err) } // Write 15 bytes 9 times : 150 bytes - for x := byte(0); x < 9; x++ { + for x := 0; x < 9; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -321,7 +324,7 @@ func TestFreezerRepairDanglingIndex(t *testing.T) { // File sizes should be 45, 45, 45 : items[3, 3, 3) } // Crop third file - fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.2.rdat", fname)) + fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0002.rdat", fname)) // Truncate third file: 45 ,45, 20 { if err := assertFileSize(fileToCrop, 45); err != nil { @@ -365,7 +368,7 @@ func TestFreezerTruncate(t *testing.T) { t.Fatal(err) } // Write 15 bytes 30 times - for x := byte(0); x < 30; x++ { + for x := 0; x < 30; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -416,7 +419,7 @@ func TestFreezerRepairFirstFile(t *testing.T) { f.Close() } // Truncate the file in half - fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.1.rdat", fname)) + fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0001.rdat", fname)) { if err := assertFileSize(fileToCrop, 40); err != nil { t.Fatal(err) @@ -463,7 +466,7 @@ func TestFreezerReadAndTruncate(t *testing.T) { t.Fatal(err) } // Write 15 bytes 30 times - for x := byte(0); x < 30; x++ { + for x := 0; x < 30; x++ { data := getChunk(15, x) f.Append(uint64(x), data) } @@ -489,7 +492,7 @@ func TestFreezerReadAndTruncate(t *testing.T) { // Now, truncate back to zero f.truncate(0) // Write the data again - for x := byte(0); x < 30; x++ { + for x := 0; x < 30; x++ { data := getChunk(15, ^x) if err := f.Append(uint64(x), data); err != nil { t.Fatalf("error %v", err) @@ -499,6 +502,101 @@ func TestFreezerReadAndTruncate(t *testing.T) { } } +func TestOffset(t *testing.T) { + t.Parallel() + wm, rm := metrics.NewMeter(), metrics.NewMeter() + fname := fmt.Sprintf("offset-%d", rand.Uint64()) + { // Fill table + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 40, true) + if err != nil { + t.Fatal(err) + } + // Write 6 x 20 bytes, splitting out into three files + f.Append(0, getChunk(20, 0xFF)) + f.Append(1, getChunk(20, 0xEE)) + + f.Append(2, getChunk(20, 0xdd)) + f.Append(3, getChunk(20, 0xcc)) + + f.Append(4, getChunk(20, 0xbb)) + f.Append(5, getChunk(20, 0xaa)) + f.printIndex() + f.Close() + } + // Now crop it. + { + // delete files 0 and 1 + for i := 0; i < 2; i++ { + p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.%04d.rdat", fname, i)) + if err := os.Remove(p); err != nil { + t.Fatal(err) + } + } + // Read the index file + p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname)) + indexFile, err := os.OpenFile(p, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + indexBuf := make([]byte, 7*indexEntrySize) + indexFile.Read(indexBuf) + + // Update the index file, so that we store + // [ file = 2, offset = 4 ] at index zero + + tailId := uint32(2) // First file is 2 + itemOffset := uint32(4) // We have removed four items + zeroIndex := indexEntry{ + offset: tailId, + filenum: itemOffset, + } + buf := zeroIndex.marshallBinary() + // Overwrite index zero + copy(indexBuf, buf) + // Remove the four next indices by overwriting + copy(indexBuf[indexEntrySize:], indexBuf[indexEntrySize*5:]) + indexFile.WriteAt(indexBuf, 0) + // Need to truncate the moved index items + indexFile.Truncate(indexEntrySize * (1 + 2)) + indexFile.Close() + + } + // Now open again + { + f, err := newCustomTable(os.TempDir(), fname, rm, wm, 40, true) + if err != nil { + t.Fatal(err) + } + f.printIndex() + // It should allow writing item 6 + f.Append(6, getChunk(20, 0x99)) + + // It should be fine to fetch 4,5,6 + if got, err := f.Retrieve(4); err != nil { + t.Fatal(err) + } else if exp := getChunk(20, 0xbb); !bytes.Equal(got, exp) { + t.Fatalf("expected %x got %x", exp, got) + } + if got, err := f.Retrieve(5); err != nil { + t.Fatal(err) + } else if exp := getChunk(20, 0xaa); !bytes.Equal(got, exp) { + t.Fatalf("expected %x got %x", exp, got) + } + if got, err := f.Retrieve(6); err != nil { + t.Fatal(err) + } else if exp := getChunk(20, 0x99); !bytes.Equal(got, exp) { + t.Fatalf("expected %x got %x", exp, got) + } + + // It should error at 0, 1,2,3 + for i := 0; i < 4; i++ { + if _, err := f.Retrieve(uint64(i)); err == nil { + t.Fatal("expected err") + } + } + } +} + // TODO (?) // - test that if we remove several head-files, aswell as data last data-file, // the index is truncated accordingly From 42c746d6f405deb0c49d868dcc6e0afe279e19ab Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 3 May 2019 12:55:36 +0200 Subject: [PATCH 23/34] freezer: disable compression on hashes and difficulties (#14) * freezer: disable compression on hashes and difficulties * core/rawdb: address review concerns * core/rawdb: address review concerns --- core/rawdb/freezer.go | 4 ++-- core/rawdb/freezer_table.go | 6 +++--- core/rawdb/schema.go | 10 ++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 84426d8ae9..21a6055cd4 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -89,8 +89,8 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { tables: make(map[string]*freezerTable), instanceLock: lock, } - for _, name := range []string{freezerHashTable, freezerHeaderTable, freezerBodiesTable, freezerReceiptTable, freezerDifficultyTable} { - table, err := newTable(datadir, name, readMeter, writeMeter) + for name, disableSnappy := range freezerNoSnappy { + table, err := newTable(datadir, name, readMeter, writeMeter, disableSnappy) if err != nil { for _, table := range freezer.tables { table.Close() diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index 93636a5ba7..d46597f730 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -97,9 +97,9 @@ type freezerTable struct { lock sync.RWMutex // Mutex protecting the data file descriptors } -// newTable opens a freezer table with default settings - 2G files and snappy compression -func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter) (*freezerTable, error) { - return newCustomTable(path, name, readMeter, writeMeter, 2*1000*1000*1000, false) +// newTable opens a freezer table with default settings - 2G files +func newTable(path string, name string, readMeter metrics.Meter, writeMeter metrics.Meter, disableSnappy bool) (*freezerTable, error) { + return newCustomTable(path, name, readMeter, writeMeter, 2*1000*1000*1000, disableSnappy) } // newCustomTable opens a freezer table, creating the data and index files if they are diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index ebca172524..a44a2c99f9 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -80,6 +80,16 @@ const ( freezerDifficultyTable = "diffs" ) +// freezerNoSnappy configures whether compression is disabled for the ancient-tables. +// Hashes and difficulties don't compress well. +var freezerNoSnappy = map[string]bool{ + freezerHeaderTable: false, + freezerHashTable: true, + freezerBodiesTable: false, + freezerReceiptTable: false, + freezerDifficultyTable: true, +} + // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary // fields. type LegacyTxLookupEntry struct { From 37d280da411eb649ce22ab69827ac5aacd46534b Mon Sep 17 00:00:00 2001 From: gary rong Date: Tue, 14 May 2019 22:07:44 +0800 Subject: [PATCH 24/34] core, cmd, vendor: fixes and database inspection tool (#15) * core, eth: some fixes for freezer * vendor, core/rawdb, cmd/geth: add db inspector * core, cmd/utils: check ancient store path forceily * cmd/geth, common, core/rawdb: a few fixes * cmd/geth: support windows file rename and fix rename error * core: support ancient plugin * core, cmd: streaming file copy * cmd, consensus, core, tests: keep genesis in leveldb * core: write txlookup during ancient init * core: bump database version --- cmd/geth/chaincmd.go | 214 ++++++++- cmd/geth/main.go | 2 + cmd/geth/os_unix.go | 51 ++ cmd/geth/os_windows.go | 43 ++ cmd/utils/cmd.go | 2 + cmd/utils/flags.go | 2 +- common/size.go | 12 +- core/blockchain.go | 105 +++- core/blockchain_test.go | 72 +-- core/genesis.go | 17 + core/headerchain.go | 11 +- core/rawdb/accessors_chain.go | 26 +- core/rawdb/accessors_metadata.go | 14 + core/rawdb/database.go | 134 ++++++ core/rawdb/freezer.go | 21 + core/rawdb/freezer_table.go | 13 + core/rawdb/schema.go | 3 + core/rawdb/table.go | 6 + eth/downloader/downloader.go | 24 +- ethdb/database.go | 5 +- .../tablewriter/{LICENCE.md => LICENSE.md} | 2 +- .../olekukonko/tablewriter/README.md | 123 ++++- .../olekukonko/tablewriter/table.go | 452 +++++++++++++----- .../tablewriter/table_with_color.go | 134 ++++++ .../olekukonko/tablewriter/test.csv | 4 - .../olekukonko/tablewriter/test_info.csv | 4 - .../github.com/olekukonko/tablewriter/util.go | 31 +- .../github.com/olekukonko/tablewriter/wrap.go | 16 +- vendor/vendor.json | 6 +- 29 files changed, 1294 insertions(+), 255 deletions(-) create mode 100644 cmd/geth/os_unix.go create mode 100644 cmd/geth/os_windows.go rename vendor/github.com/olekukonko/tablewriter/{LICENCE.md => LICENSE.md} (98%) create mode 100644 vendor/github.com/olekukonko/tablewriter/table_with_color.go delete mode 100644 vendor/github.com/olekukonko/tablewriter/test.csv delete mode 100644 vendor/github.com/olekukonko/tablewriter/test_info.csv diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 809f5cf4a1..70164f82b2 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -18,8 +18,12 @@ package main import ( "encoding/json" + "errors" "fmt" + "io" + "io/ioutil" "os" + "path/filepath" "runtime" "strconv" "sync/atomic" @@ -167,6 +171,37 @@ Remove blockchain and state databases`, The arguments are interpreted as block numbers or hashes. Use "ethereum dump 0" to dump the genesis block.`, } + migrateAncientCommand = cli.Command{ + Action: utils.MigrateFlags(migrateAncient), + Name: "migrate-ancient", + Usage: "migrate ancient database forcibly", + ArgsUsage: " ", + Flags: []cli.Flag{ + utils.DataDirFlag, + utils.AncientFlag, + utils.CacheFlag, + utils.TestnetFlag, + utils.RinkebyFlag, + utils.GoerliFlag, + }, + Category: "BLOCKCHAIN COMMANDS", + } + inspectCommand = cli.Command{ + Action: utils.MigrateFlags(inspect), + Name: "inspect", + Usage: "Inspect the storage size for each type of data in the database", + ArgsUsage: " ", + Flags: []cli.Flag{ + utils.DataDirFlag, + utils.AncientFlag, + utils.CacheFlag, + utils.TestnetFlag, + utils.RinkebyFlag, + utils.GoerliFlag, + utils.SyncModeFlag, + }, + Category: "BLOCKCHAIN COMMANDS", + } ) // initGenesis will initialise the given JSON format genesis file and writes it as @@ -423,29 +458,48 @@ func copyDb(ctx *cli.Context) error { } func removeDB(ctx *cli.Context) error { - stack, _ := makeConfigNode(ctx) + stack, config := makeConfigNode(ctx) - for _, name := range []string{"chaindata", "lightchaindata"} { + for i, name := range []string{"chaindata", "lightchaindata"} { // Ensure the database exists in the first place logger := log.New("database", name) + var ( + dbdirs []string + freezer string + ) dbdir := stack.ResolvePath(name) if !common.FileExist(dbdir) { logger.Info("Database doesn't exist, skipping", "path", dbdir) continue } - // Confirm removal and execute - fmt.Println(dbdir) - confirm, err := console.Stdin.PromptConfirm("Remove this database?") - switch { - case err != nil: - utils.Fatalf("%v", err) - case !confirm: - logger.Warn("Database deletion aborted") - default: - start := time.Now() - os.RemoveAll(dbdir) - logger.Info("Database successfully deleted", "elapsed", common.PrettyDuration(time.Since(start))) + dbdirs = append(dbdirs, dbdir) + if i == 0 { + freezer = config.Eth.DatabaseFreezer + switch { + case freezer == "": + freezer = filepath.Join(dbdir, "ancient") + case !filepath.IsAbs(freezer): + freezer = config.Node.ResolvePath(freezer) + } + if common.FileExist(freezer) { + dbdirs = append(dbdirs, freezer) + } + } + for i := len(dbdirs) - 1; i >= 0; i-- { + // Confirm removal and execute + fmt.Println(dbdirs[i]) + confirm, err := console.Stdin.PromptConfirm("Remove this database?") + switch { + case err != nil: + utils.Fatalf("%v", err) + case !confirm: + logger.Warn("Database deletion aborted") + default: + start := time.Now() + os.RemoveAll(dbdirs[i]) + logger.Info("Database successfully deleted", "elapsed", common.PrettyDuration(time.Since(start))) + } } } return nil @@ -479,8 +533,140 @@ func dump(ctx *cli.Context) error { return nil } +func migrateAncient(ctx *cli.Context) error { + node, config := makeConfigNode(ctx) + defer node.Close() + + dbdir := config.Node.ResolvePath("chaindata") + kvdb, err := rawdb.NewLevelDBDatabase(dbdir, 128, 1024, "") + if err != nil { + return err + } + defer kvdb.Close() + + freezer := config.Eth.DatabaseFreezer + switch { + case freezer == "": + freezer = filepath.Join(dbdir, "ancient") + case !filepath.IsAbs(freezer): + freezer = config.Node.ResolvePath(freezer) + } + stored := rawdb.ReadAncientPath(kvdb) + if stored != freezer && stored != "" { + confirm, err := console.Stdin.PromptConfirm(fmt.Sprintf("Are you sure to migrate ancient database from %s to %s?", stored, freezer)) + switch { + case err != nil: + utils.Fatalf("%v", err) + case !confirm: + log.Warn("Ancient database migration aborted") + default: + if err := rename(stored, freezer); err != nil { + // Renaming a file can fail if the source and destination + // are on different file systems. + if err := moveAncient(stored, freezer); err != nil { + utils.Fatalf("Migrate ancient database failed, %v", err) + } + } + rawdb.WriteAncientPath(kvdb, freezer) + log.Info("Ancient database successfully migrated") + } + } + return nil +} + +func inspect(ctx *cli.Context) error { + node, _ := makeConfigNode(ctx) + defer node.Close() + + _, chainDb := utils.MakeChain(ctx, node) + defer chainDb.Close() + + return rawdb.InspectDatabase(chainDb) +} + // hashish returns true for strings that look like hashes. func hashish(x string) bool { _, err := strconv.Atoi(x) return err != nil } + +// copyFileSynced copies data from source file to destination +// and synces the dest file forcibly. +func copyFileSynced(src string, dest string, info os.FileInfo) error { + srcf, err := os.Open(src) + if err != nil { + return err + } + defer srcf.Close() + + destf, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode().Perm()) + if err != nil { + return err + } + // The maximum size of ancient file is 2GB, 4MB buffer is suitable here. + buff := make([]byte, 4*1024*1024) + for { + rn, err := srcf.Read(buff) + if err != nil && err != io.EOF { + return err + } + if rn == 0 { + break + } + if wn, err := destf.Write(buff[:rn]); err != nil || wn != rn { + return err + } + } + if err1 := destf.Sync(); err == nil { + err = err1 + } + if err1 := destf.Close(); err == nil { + err = err1 + } + return err +} + +// copyDirSynced recursively copies files under the specified dir +// to dest and synces the dest dir forcibly. +func copyDirSynced(src string, dest string, info os.FileInfo) error { + if err := os.MkdirAll(dest, os.ModePerm); err != nil { + return err + } + defer os.Chmod(dest, info.Mode()) + + objects, err := ioutil.ReadDir(src) + if err != nil { + return err + } + for _, obj := range objects { + // All files in ancient database should be flatten files. + if !obj.Mode().IsRegular() { + continue + } + subsrc, subdest := filepath.Join(src, obj.Name()), filepath.Join(dest, obj.Name()) + if err := copyFileSynced(subsrc, subdest, obj); err != nil { + return err + } + } + return syncDir(dest) +} + +// moveAncient migrates ancient database from source to destination. +func moveAncient(src string, dest string) error { + srcinfo, err := os.Stat(src) + if err != nil { + return err + } + if !srcinfo.IsDir() { + return errors.New("ancient directory expected") + } + if destinfo, err := os.Lstat(dest); !os.IsNotExist(err) { + if destinfo.Mode()&os.ModeSymlink != 0 { + return errors.New("symbolic link datadir is not supported") + } + } + if err := copyDirSynced(src, dest, srcinfo); err != nil { + return err + } + return os.RemoveAll(src) +} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index dc63f23026..afa39bf933 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -204,6 +204,8 @@ func init() { copydbCommand, removedbCommand, dumpCommand, + migrateAncientCommand, + inspectCommand, // See accountcmd.go: accountCommand, walletCommand, diff --git a/cmd/geth/os_unix.go b/cmd/geth/os_unix.go new file mode 100644 index 0000000000..6722ec9cb6 --- /dev/null +++ b/cmd/geth/os_unix.go @@ -0,0 +1,51 @@ +// Copyright (c) 2012, Suryandaru Triandana +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license. +// +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package main + +import ( + "os" + "syscall" +) + +func rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +func isErrInvalid(err error) bool { + if err == os.ErrInvalid { + return true + } + // Go < 1.8 + if syserr, ok := err.(*os.SyscallError); ok && syserr.Err == syscall.EINVAL { + return true + } + // Go >= 1.8 returns *os.PathError instead + if patherr, ok := err.(*os.PathError); ok && patherr.Err == syscall.EINVAL { + return true + } + return false +} + +func syncDir(name string) error { + // As per fsync manpage, Linux seems to expect fsync on directory, however + // some system don't support this, so we will ignore syscall.EINVAL. + // + // From fsync(2): + // Calling fsync() does not necessarily ensure that the entry in the + // directory containing the file has also reached disk. For that an + // explicit fsync() on a file descriptor for the directory is also needed. + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + if err := f.Sync(); err != nil && !isErrInvalid(err) { + return err + } + return nil +} diff --git a/cmd/geth/os_windows.go b/cmd/geth/os_windows.go new file mode 100644 index 0000000000..f2406ec9bc --- /dev/null +++ b/cmd/geth/os_windows.go @@ -0,0 +1,43 @@ +// Copyright (c) 2013, Suryandaru Triandana +// All rights reserved. +// +// Use of this source code is governed by a BSD-style license. + +package main + +import ( + "syscall" + "unsafe" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + procMoveFileExW = modkernel32.NewProc("MoveFileExW") +) + +const _MOVEFILE_REPLACE_EXISTING = 1 + +func moveFileEx(from *uint16, to *uint16, flags uint32) error { + r1, _, e1 := syscall.Syscall(procMoveFileExW.Addr(), 3, uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(to)), uintptr(flags)) + if r1 == 0 { + if e1 != 0 { + return error(e1) + } + return syscall.EINVAL + } + return nil +} + +func rename(oldpath, newpath string) error { + from, err := syscall.UTF16PtrFromString(oldpath) + if err != nil { + return err + } + to, err := syscall.UTF16PtrFromString(newpath) + if err != nil { + return err + } + return moveFileEx(from, to, _MOVEFILE_REPLACE_EXISTING) +} + +func syncDir(name string) error { return nil } diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go index 74a8c7f394..a3ee45ba7f 100644 --- a/cmd/utils/cmd.go +++ b/cmd/utils/cmd.go @@ -302,6 +302,8 @@ func ExportPreimages(db ethdb.Database, fn string) error { } // Iterate over the preimages and export them it := db.NewIteratorWithPrefix([]byte("secure-key-")) + defer it.Release() + for it.Next() { if err := rlp.Encode(writer, it.Value()); err != nil { return err diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c40da85b05..ddeb44f346 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1573,7 +1573,7 @@ func MakeChainDatabase(ctx *cli.Context, stack *node.Node) ethdb.Database { if ctx.GlobalString(SyncModeFlag.Name) == "light" { name = "lightchaindata" } - chainDb, err := stack.OpenDatabaseWithFreezer(name, cache, handles, "", "") + chainDb, err := stack.OpenDatabaseWithFreezer(name, cache, handles, ctx.GlobalString(AncientFlag.Name), "") if err != nil { Fatalf("Could not open database: %v", err) } diff --git a/common/size.go b/common/size.go index 6381499a48..097b6304a8 100644 --- a/common/size.go +++ b/common/size.go @@ -26,7 +26,11 @@ type StorageSize float64 // String implements the stringer interface. func (s StorageSize) String() string { - if s > 1048576 { + if s > 1099511627776 { + return fmt.Sprintf("%.2f TiB", s/1099511627776) + } else if s > 1073741824 { + return fmt.Sprintf("%.2f GiB", s/1073741824) + } else if s > 1048576 { return fmt.Sprintf("%.2f MiB", s/1048576) } else if s > 1024 { return fmt.Sprintf("%.2f KiB", s/1024) @@ -38,7 +42,11 @@ func (s StorageSize) String() string { // TerminalString implements log.TerminalStringer, formatting a string for console // output during logging. func (s StorageSize) TerminalString() string { - if s > 1048576 { + if s > 1099511627776 { + return fmt.Sprintf("%.2fTiB", s/1099511627776) + } else if s > 1073741824 { + return fmt.Sprintf("%.2fGiB", s/1073741824) + } else if s > 1048576 { return fmt.Sprintf("%.2fMiB", s/1048576) } else if s > 1024 { return fmt.Sprintf("%.2fKiB", s/1024) diff --git a/core/blockchain.go b/core/blockchain.go index 4ac2c3a44e..651c67c5d6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -93,7 +93,10 @@ const ( // - Version 6 // The following incompatible database changes were added: // * Transaction lookup information stores the corresponding block number instead of block hash - BlockChainVersion uint64 = 6 + // - Version 7 + // The following incompatible database changes were added: + // * Use freezer as the ancient database to maintain all ancient data + BlockChainVersion uint64 = 7 ) // CacheConfig contains the configuration values for the trie caching/pruning @@ -215,10 +218,35 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par if bc.genesisBlock == nil { return nil, ErrNoGenesis } + // Initialize the chain with ancient data if it isn't empty. + if bc.empty() { + if frozen, err := bc.db.Ancients(); err == nil && frozen > 0 { + for i := uint64(0); i < frozen; i++ { + // Inject hash<->number mapping. + hash := rawdb.ReadCanonicalHash(bc.db, i) + if hash == (common.Hash{}) { + return nil, errors.New("broken ancient database") + } + rawdb.WriteHeaderNumber(bc.db, hash, i) + + // Inject txlookup indexes. + block := rawdb.ReadBlock(bc.db, hash, i) + if block == nil { + return nil, errors.New("broken ancient database") + } + rawdb.WriteTxLookupEntries(bc.db, block) + } + hash := rawdb.ReadCanonicalHash(bc.db, frozen-1) + rawdb.WriteHeadHeaderHash(bc.db, hash) + rawdb.WriteHeadFastBlockHash(bc.db, hash) + + log.Info("Initialized chain with ancients", "number", frozen-1, "hash", hash) + } + } if err := bc.loadLastState(); err != nil { return nil, err } - if frozen, err := bc.db.Ancients(); err == nil && frozen >= 1 { + if frozen, err := bc.db.Ancients(); err == nil && frozen > 0 { var ( needRewind bool low uint64 @@ -278,6 +306,20 @@ func (bc *BlockChain) GetVMConfig() *vm.Config { return &bc.vmConfig } +// empty returns an indicator whether the blockchain is empty. +// Note, it's a special case that we connect a non-empty ancient +// database with an empty node, so that we can plugin the ancient +// into node seamlessly. +func (bc *BlockChain) empty() bool { + genesis := bc.genesisBlock.Hash() + for _, hash := range []common.Hash{rawdb.ReadHeadBlockHash(bc.db), rawdb.ReadHeadHeaderHash(bc.db), rawdb.ReadHeadFastBlockHash(bc.db)} { + if hash != genesis { + return false + } + } + return true +} + // loadLastState loads the last known chain state from the database. This method // assumes that the chain manager mutex is held. func (bc *BlockChain) loadLastState() error { @@ -383,7 +425,9 @@ func (bc *BlockChain) SetHead(head uint64) error { if num+1 <= frozen { // Truncate all relative data(header, total difficulty, body, receipt // and canonical hash) from ancient store. - bc.db.TruncateAncients(num + 1) + if err := bc.db.TruncateAncients(num + 1); err != nil { + log.Crit("Failed to truncate ancient data", "number", num, "err", err) + } // Remove the hash <-> number mapping from the active store. rawdb.DeleteHeaderNumber(db, hash) @@ -948,6 +992,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [ } } }() + var deleted types.Blocks for i, block := range blockChain { // Short circuit insertion if shutting down or processing failed if atomic.LoadInt32(&bc.procInterrupt) == 1 { @@ -961,16 +1006,38 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [ if !bc.HasHeader(block.Hash(), block.NumberU64()) { return i, fmt.Errorf("containing header #%d [%x…] unknown", block.Number(), block.Hash().Bytes()[:4]) } - // Compute all the non-consensus fields of the receipts - if err := receiptChain[i].DeriveFields(bc.chainConfig, block.Hash(), block.NumberU64(), block.Transactions()); err != nil { - return i, fmt.Errorf("failed to derive receipts data: %v", err) + var ( + start = time.Now() + logged = time.Now() + count int + ) + // Migrate all ancient blocks. This can happen if someone upgrades from Geth + // 1.8.x to 1.9.x mid-fast-sync. Perhaps we can get rid of this path in the + // long term. + for { + // We can ignore the error here since light client won't hit this code path. + frozen, _ := bc.db.Ancients() + if frozen >= block.NumberU64() { + break + } + h := rawdb.ReadCanonicalHash(bc.db, frozen) + b := rawdb.ReadBlock(bc.db, h, frozen) + size += rawdb.WriteAncientBlock(bc.db, b, rawdb.ReadReceipts(bc.db, h, frozen, bc.chainConfig), rawdb.ReadTd(bc.db, h, frozen)) + count += 1 + + // Always keep genesis block in active database. + if b.NumberU64() != 0 { + deleted = append(deleted, b) + } + if time.Since(logged) > 8*time.Second { + log.Info("Migrating ancient blocks", "count", count, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } } - // Initialize freezer with genesis block first - if frozen, err := bc.db.Ancients(); err == nil && frozen == 0 && block.NumberU64() == 1 { - genesisBlock := rawdb.ReadBlock(bc.db, rawdb.ReadCanonicalHash(bc.db, 0), 0) - size += rawdb.WriteAncientBlock(bc.db, genesisBlock, nil, genesisBlock.Difficulty()) + if count > 0 { + log.Info("Migrated ancient blocks", "count", count, "elapsed", common.PrettyDuration(time.Since(start))) } - // Flush data into ancient store. + // Flush data into ancient database. size += rawdb.WriteAncientBlock(bc.db, block, receiptChain[i], bc.GetTd(block.Hash(), block.NumberU64())) rawdb.WriteTxLookupEntries(batch, block) @@ -992,15 +1059,8 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [ } previous = nil // disable rollback explicitly - // Remove the ancient data from the active store - cleanGenesis := len(blockChain) > 0 && blockChain[0].NumberU64() == 1 - if cleanGenesis { - // Migrate genesis block to ancient store too. - rawdb.DeleteBlockWithoutNumber(batch, rawdb.ReadCanonicalHash(bc.db, 0), 0) - rawdb.DeleteCanonicalHash(batch, 0) - } // Wipe out canonical block data. - for _, block := range blockChain { + for _, block := range append(deleted, blockChain...) { rawdb.DeleteBlockWithoutNumber(batch, block.Hash(), block.NumberU64()) rawdb.DeleteCanonicalHash(batch, block.NumberU64()) } @@ -1008,8 +1068,9 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [ return 0, err } batch.Reset() + // Wipe out side chain too. - for _, block := range blockChain { + for _, block := range append(deleted, blockChain...) { for _, hash := range rawdb.ReadAllHashes(bc.db, block.NumberU64()) { rawdb.DeleteBlock(batch, hash, block.NumberU64()) } @@ -1035,10 +1096,6 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [ stats.ignored++ continue } - // Compute all the non-consensus fields of the receipts - if err := receiptChain[i].DeriveFields(bc.chainConfig, block.Hash(), block.NumberU64(), block.Transactions()); err != nil { - return i, fmt.Errorf("failed to derive receipts data: %v", err) - } // Write all the data out into the database rawdb.WriteBody(batch, block.Hash(), block.NumberU64(), block.Body()) rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receiptChain[i]) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 7b1a9a54f4..09caf7e602 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -716,6 +716,20 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { height := uint64(1024) blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), nil) + // makeDb creates a db instance for testing. + makeDb := func() (ethdb.Database, func()) { + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(dir) + db, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), dir, "") + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(db) + return db, func() { os.RemoveAll(dir) } + } // Configure a subchain to roll back remove := []common.Hash{} for _, block := range blocks[height/2:] { @@ -734,9 +748,8 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { } } // Import the chain as an archive node and ensure all pointers are updated - archiveDb := rawdb.NewMemoryDatabase() - gspec.MustCommit(archiveDb) - + archiveDb, delfn := makeDb() + defer delfn() archive, _ := NewBlockChain(archiveDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) if n, err := archive.InsertChain(blocks); err != nil { t.Fatalf("failed to process block %d: %v", n, err) @@ -748,8 +761,8 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { assert(t, "archive", archive, height/2, height/2, height/2) // Import the chain as a non-archive node and ensure all pointers are updated - fastDb := rawdb.NewMemoryDatabase() - gspec.MustCommit(fastDb) + fastDb, delfn := makeDb() + defer delfn() fast, _ := NewBlockChain(fastDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer fast.Stop() @@ -768,16 +781,8 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { assert(t, "fast", fast, height/2, height/2, 0) // Import the chain as a ancient-first node and ensure all pointers are updated - frdir, err := ioutil.TempDir("", "") - if err != nil { - t.Fatalf("failed to create temp freezer dir: %v", err) - } - defer os.Remove(frdir) - ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "") - if err != nil { - t.Fatalf("failed to create temp freezer db: %v", err) - } - gspec.MustCommit(ancientDb) + ancientDb, delfn := makeDb() + defer delfn() ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer ancient.Stop() @@ -795,9 +800,8 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { } // Import the chain as a light node and ensure all pointers are updated - lightDb := rawdb.NewMemoryDatabase() - gspec.MustCommit(lightDb) - + lightDb, delfn := makeDb() + defer delfn() light, _ := NewBlockChain(lightDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) if n, err := light.InsertHeaderChain(headers, 1); err != nil { t.Fatalf("failed to insert header %d: %v", n, err) @@ -1892,10 +1896,18 @@ func testInsertKnownChainData(t *testing.T, typ string) { b.SetCoinbase(common.Address{1}) b.OffsetTime(-9) // A higher difficulty }) - // Import the shared chain and the original canonical one - chaindb := rawdb.NewMemoryDatabase() + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(dir) + chaindb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), dir, "") + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } new(Genesis).MustCommit(chaindb) + defer os.RemoveAll(dir) chain, err := NewBlockChain(chaindb, nil, params.TestChainConfig, engine, vm.Config{}, nil) if err != nil { @@ -1992,18 +2004,16 @@ func testInsertKnownChainData(t *testing.T, typ string) { // The head shouldn't change. asserter(t, blocks3[len(blocks3)-1]) - if typ != "headers" { - // Rollback the heavier chain and re-insert the longer chain again - for i := 0; i < len(blocks3); i++ { - rollback = append(rollback, blocks3[i].Hash()) - } - chain.Rollback(rollback) - - if err := inserter(append(blocks, blocks2...), append(receipts, receipts2...)); err != nil { - t.Fatalf("failed to insert chain data: %v", err) - } - asserter(t, blocks2[len(blocks2)-1]) + // Rollback the heavier chain and re-insert the longer chain again + for i := 0; i < len(blocks3); i++ { + rollback = append(rollback, blocks3[i].Hash()) } + chain.Rollback(rollback) + + if err := inserter(append(blocks, blocks2...), append(receipts, receipts2...)); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + asserter(t, blocks2[len(blocks2)-1]) } // getLongAndShortChains returns two chains, diff --git a/core/genesis.go b/core/genesis.go index 1f34a3a9ea..830fb033b8 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -170,6 +170,22 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, genesis *Genesis, constant return genesis.Config, block.Hash(), err } + // We have the genesis block in database(perhaps in ancient database) + // but the corresponding state is missing. + header := rawdb.ReadHeader(db, stored, 0) + if _, err := state.New(header.Root, state.NewDatabaseWithCache(db, 0)); err != nil { + if genesis == nil { + genesis = DefaultGenesisBlock() + } + // Ensure the stored genesis matches with the given one. + hash := genesis.ToBlock(nil).Hash() + if hash != stored { + return genesis.Config, hash, &GenesisMismatchError{stored, hash} + } + block, err := genesis.Commit(db) + return genesis.Config, block.Hash(), err + } + // Check whether the genesis block is already written. if genesis != nil { hash := genesis.ToBlock(nil).Hash() @@ -277,6 +293,7 @@ func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) { rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), nil) rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64()) rawdb.WriteHeadBlockHash(db, block.Hash()) + rawdb.WriteHeadFastBlockHash(db, block.Hash()) rawdb.WriteHeadHeaderHash(db, block.Hash()) config := g.Config diff --git a/core/headerchain.go b/core/headerchain.go index 659141fd13..cdd64bb505 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -274,9 +274,14 @@ func (hc *HeaderChain) InsertHeaderChain(chain []*types.Header, writeHeader WhCa return i, errors.New("aborted") } // If the header's already known, skip it, otherwise store - if hc.HasHeader(header.Hash(), header.Number.Uint64()) { - stats.ignored++ - continue + hash := header.Hash() + if hc.HasHeader(hash, header.Number.Uint64()) { + externTd := hc.GetTd(hash, header.Number.Uint64()) + localTd := hc.GetTd(hc.currentHeaderHash, hc.CurrentHeader().Number.Uint64()) + if externTd == nil || externTd.Cmp(localTd) <= 0 { + stats.ignored++ + continue + } } if err := writeHeader(header); err != nil { return i, err diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 681e6e9171..fab7ca56c3 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -89,7 +89,16 @@ func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 { return &number } -// DeleteHeaderNumber removes hash to number mapping. +// WriteHeaderNumber stores the hash->number mapping. +func WriteHeaderNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { + key := headerNumberKey(hash) + enc := encodeBlockNumber(number) + if err := db.Put(key, enc); err != nil { + log.Crit("Failed to store hash to number mapping", "err", err) + } +} + +// DeleteHeaderNumber removes hash->number mapping. func DeleteHeaderNumber(db ethdb.KeyValueWriter, hash common.Hash) { if err := db.Delete(headerNumberKey(hash)); err != nil { log.Crit("Failed to delete hash to number mapping", "err", err) @@ -206,22 +215,19 @@ func ReadHeader(db ethdb.Reader, hash common.Hash, number uint64) *types.Header // WriteHeader stores a block header into the database and also stores the hash- // to-number mapping. func WriteHeader(db ethdb.KeyValueWriter, header *types.Header) { - // Write the hash -> number mapping var ( - hash = header.Hash() - number = header.Number.Uint64() - encoded = encodeBlockNumber(number) + hash = header.Hash() + number = header.Number.Uint64() ) - key := headerNumberKey(hash) - if err := db.Put(key, encoded); err != nil { - log.Crit("Failed to store hash to number mapping", "err", err) - } + // Write the hash -> number mapping + WriteHeaderNumber(db, hash, number) + // Write the encoded header data, err := rlp.EncodeToBytes(header) if err != nil { log.Crit("Failed to RLP encode header", "err", err) } - key = headerKey(number, hash) + key := headerKey(number, hash) if err := db.Put(key, data); err != nil { log.Crit("Failed to store header", "err", err) } diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go index f8d09fbddf..e6235f0104 100644 --- a/core/rawdb/accessors_metadata.go +++ b/core/rawdb/accessors_metadata.go @@ -80,6 +80,20 @@ func WriteChainConfig(db ethdb.KeyValueWriter, hash common.Hash, cfg *params.Cha } } +// ReadAncientPath retrieves ancient database path which is recorded during the +// first node setup or forcibly changed by user. +func ReadAncientPath(db ethdb.KeyValueReader) string { + data, _ := db.Get(ancientKey) + return string(data) +} + +// WriteAncientPath writes ancient database path into the key-value database. +func WriteAncientPath(db ethdb.KeyValueWriter, path string) { + if err := db.Put(ancientKey, []byte(path)); err != nil { + log.Crit("Failed to store ancient path", "err", err) + } +} + // ReadPreimage retrieves a single preimage of the provided hash. func ReadPreimage(db ethdb.KeyValueReader, hash common.Hash) []byte { data, _ := db.Get(preimageKey(hash)) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 5a3c7f94b5..016c6c9094 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -17,11 +17,17 @@ package rawdb import ( + "bytes" "fmt" + "os" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/leveldb" "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/log" + "github.com/olekukonko/tablewriter" ) // freezerdb is a database wrapper that enabled freezer data retrievals. @@ -66,6 +72,11 @@ func (db *nofreezedb) Ancients() (uint64, error) { return 0, errNotSupported } +// AncientSize returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) AncientSize(kind string) (uint64, error) { + return 0, errNotSupported +} + // AppendAncient returns an error as we don't have a backing chain freezer. func (db *nofreezedb) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { return errNotSupported @@ -140,5 +151,128 @@ func NewLevelDBDatabaseWithFreezer(file string, cache int, handles int, freezer kvdb.Close() return nil, err } + // Make sure we always use the same ancient store. + // + // | stored == nil | stored != nil + // ----------------+------------------+---------------------- + // freezer == nil | non-freezer mode | ancient store missing + // freezer != nil | initialize | ensure consistency + stored := ReadAncientPath(kvdb) + if stored == "" && freezer != "" { + WriteAncientPath(kvdb, freezer) + } else if stored != freezer { + log.Warn("Ancient path mismatch", "stored", stored, "given", freezer) + log.Crit("Please use a consistent ancient path or migrate it via the command line tool `geth migrate-ancient`") + } return frdb, nil } + +// InspectDatabase traverses the entire database and checks the size +// of all different categories of data. +func InspectDatabase(db ethdb.Database) error { + it := db.NewIterator() + defer it.Release() + + var ( + count int64 + start = time.Now() + logged = time.Now() + + // Key-value store statistics + total common.StorageSize + headerSize common.StorageSize + bodySize common.StorageSize + receiptSize common.StorageSize + tdSize common.StorageSize + numHashPairing common.StorageSize + hashNumPairing common.StorageSize + trieSize common.StorageSize + txlookupSize common.StorageSize + preimageSize common.StorageSize + bloomBitsSize common.StorageSize + + // Ancient store statistics + ancientHeaders common.StorageSize + ancientBodies common.StorageSize + ancientReceipts common.StorageSize + ancientHashes common.StorageSize + ancientTds common.StorageSize + + // Les statistic + ChtTrieNodes common.StorageSize + BloomTrieNodes common.StorageSize + ) + // Inspect key-value database first. + for it.Next() { + var ( + key = it.Key() + size = common.StorageSize(len(key) + len(it.Value())) + ) + total += size + switch { + case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix): + tdSize += size + case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix): + numHashPairing += size + case bytes.HasPrefix(key, headerPrefix) && len(key) == (len(headerPrefix)+8+common.HashLength): + headerSize += size + case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): + hashNumPairing += size + case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength): + bodySize += size + case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength): + receiptSize += size + case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength): + txlookupSize += size + case bytes.HasPrefix(key, preimagePrefix) && len(key) == (len(preimagePrefix)+common.HashLength): + preimageSize += size + case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength): + bloomBitsSize += size + case bytes.HasPrefix(key, []byte("cht-")) && len(key) == 4+common.HashLength: + ChtTrieNodes += size + case bytes.HasPrefix(key, []byte("blt-")) && len(key) == 4+common.HashLength: + BloomTrieNodes += size + case len(key) == common.HashLength: + trieSize += size + } + count += 1 + if count%1000 == 0 && time.Since(logged) > 8*time.Second { + log.Info("Inspecting database", "count", count, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + // Inspect append-only file store then. + ancients := []*common.StorageSize{&ancientHeaders, &ancientBodies, &ancientReceipts, &ancientHashes, &ancientTds} + for i, category := range []string{freezerHeaderTable, freezerBodiesTable, freezerReceiptTable, freezerHashTable, freezerDifficultyTable} { + if size, err := db.AncientSize(category); err == nil { + *ancients[i] += common.StorageSize(size) + total += common.StorageSize(size) + } + } + // Display the database statistic. + stats := [][]string{ + {"Key-Value store", "Headers", headerSize.String()}, + {"Key-Value store", "Bodies", bodySize.String()}, + {"Key-Value store", "Receipts", receiptSize.String()}, + {"Key-Value store", "Difficulties", tdSize.String()}, + {"Key-Value store", "Block number->hash", numHashPairing.String()}, + {"Key-Value store", "Block hash->number", hashNumPairing.String()}, + {"Key-Value store", "Transaction index", txlookupSize.String()}, + {"Key-Value store", "Bloombit index", bloomBitsSize.String()}, + {"Key-Value store", "Trie nodes", trieSize.String()}, + {"Key-Value store", "Trie preimages", preimageSize.String()}, + {"Ancient store", "Headers", ancientHeaders.String()}, + {"Ancient store", "Bodies", ancientBodies.String()}, + {"Ancient store", "Receipts", ancientReceipts.String()}, + {"Ancient store", "Difficulties", ancientTds.String()}, + {"Ancient store", "Block number->hash", ancientHashes.String()}, + {"Light client", "CHT trie nodes", ChtTrieNodes.String()}, + {"Light client", "Bloom trie nodes", BloomTrieNodes.String()}, + } + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Database", "Category", "Size"}) + table.SetFooter([]string{"", "Total", total.String()}) + table.AppendBulk(stats) + table.Render() + return nil +} diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 21a6055cd4..f3a6bbb8f9 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "math" + "os" "path/filepath" "sync/atomic" "time" @@ -39,6 +40,10 @@ var ( // errOutOrderInsertion is returned if the user attempts to inject out-of-order // binary blobs into the freezer. errOutOrderInsertion = errors.New("the append operation is out-order") + + // errSymlinkDatadir is returned if the ancient directory specified by user + // is a symbolic link. + errSymlinkDatadir = errors.New("symbolic link datadir is not supported") ) const ( @@ -78,6 +83,13 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) ) + // Ensure the datadir is not a symbolic link if it exists. + if info, err := os.Lstat(datadir); !os.IsNotExist(err) { + if info.Mode()&os.ModeSymlink != 0 { + log.Warn("Symbolic link ancient database is not supported", "path", datadir) + return nil, errSymlinkDatadir + } + } // Leveldb uses LOCK as the filelock filename. To prevent the // name collision, we use FLOCK as the lock name. lock, _, err := fileutil.Flock(filepath.Join(datadir, "FLOCK")) @@ -107,6 +119,7 @@ func newFreezer(datadir string, namespace string) (*freezer, error) { lock.Release() return nil, err } + log.Info("Opened ancient database", "database", datadir) return freezer, nil } @@ -149,6 +162,14 @@ func (f *freezer) Ancients() (uint64, error) { return atomic.LoadUint64(&f.frozen), nil } +// AncientSize returns the ancient size of the specified category. +func (f *freezer) AncientSize(kind string) (uint64, error) { + if table := f.tables[kind]; table != nil { + return table.size() + } + return 0, errUnknownTable +} + // AppendAncient injects all binary blobs belong to block at the end of the // append-only immutable table files. // diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index d46597f730..ebccf78169 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -515,6 +515,19 @@ func (t *freezerTable) has(number uint64) bool { return atomic.LoadUint64(&t.items) > number } +// size returns the total data size in the freezer table. +func (t *freezerTable) size() (uint64, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + stat, err := t.index.Stat() + if err != nil { + return 0, err + } + total := uint64(t.maxFileSize)*uint64(t.headId-t.tailId) + uint64(t.headBytes) + uint64(stat.Size()) + return total, nil +} + // Sync pushes any pending data from memory out to disk. This is an expensive // operation, so use it with care. func (t *freezerTable) Sync() error { diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index a44a2c99f9..0d54a3c8b1 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -41,6 +41,9 @@ var ( // fastTrieProgressKey tracks the number of trie entries imported during fast sync. fastTrieProgressKey = []byte("TrieSync") + // ancientKey tracks the absolute path of ancient database. + ancientKey = []byte("AncientPath") + // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes). headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 1246789595..6610b7f5a2 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -68,6 +68,12 @@ func (t *table) Ancients() (uint64, error) { return t.db.Ancients() } +// AncientSize is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) AncientSize(kind string) (uint64, error) { + return t.db.AncientSize(kind) +} + // AppendAncient is a noop passthrough that just forwards the request to the underlying // database. func (t *table) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 79107c8d10..5c350debea 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -478,21 +478,21 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.I } if d.mode == FastSync { // Set the ancient data limitation. - // If we are running fast sync, all block data not greater than ancientLimit will - // be written to the ancient store. Otherwise, block data will be written to active - // database and then wait freezer to migrate. + // If we are running fast sync, all block data older than ancientLimit will be + // written to the ancient store. More recent data will be written to the active + // database and will wait for the freezer to migrate. // - // If there is checkpoint available, then calculate the ancientLimit through - // checkpoint. Otherwise calculate the ancient limit through the advertised - // height by remote peer. + // If there is a checkpoint available, then calculate the ancientLimit through + // that. Otherwise calculate the ancient limit through the advertised height + // of the remote peer. // - // The reason for picking checkpoint first is: there exists an attack vector - // for height that: a malicious peer can give us a fake(very high) height, - // so that the ancient limit is also very high. And then the peer start to - // feed us valid blocks until head. All of these blocks might be written into - // the ancient store, the safe region for freezer is not enough. + // The reason for picking checkpoint first is that a malicious peer can give us + // a fake (very high) height, forcing the ancient limit to also be very high. + // The peer would start to feed us valid blocks until head, resulting in all of + // the blocks might be written into the ancient store. A following mini-reorg + // could cause issues. if d.checkpoint != 0 && d.checkpoint > MaxForkAncestry+1 { - d.ancientLimit = height - MaxForkAncestry - 1 + d.ancientLimit = d.checkpoint } else if height > MaxForkAncestry+1 { d.ancientLimit = height - MaxForkAncestry - 1 } diff --git a/ethdb/database.go b/ethdb/database.go index e3eff32dba..1ba169bcfa 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -76,8 +76,11 @@ type AncientReader interface { // Ancient retrieves an ancient binary blob from the append-only immutable files. Ancient(kind string, number uint64) ([]byte, error) - // Ancients returns the ancient store length + // Ancients returns the ancient item numbers in the ancient store. Ancients() (uint64, error) + + // AncientSize returns the ancient size of the specified category. + AncientSize(kind string) (uint64, error) } // AncientWriter contains the methods required to write to immutable ancient data. diff --git a/vendor/github.com/olekukonko/tablewriter/LICENCE.md b/vendor/github.com/olekukonko/tablewriter/LICENSE.md similarity index 98% rename from vendor/github.com/olekukonko/tablewriter/LICENCE.md rename to vendor/github.com/olekukonko/tablewriter/LICENSE.md index 1fd8484253..a0769b5c15 100644 --- a/vendor/github.com/olekukonko/tablewriter/LICENCE.md +++ b/vendor/github.com/olekukonko/tablewriter/LICENSE.md @@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md index 805330adcd..92d71ed48b 100644 --- a/vendor/github.com/olekukonko/tablewriter/README.md +++ b/vendor/github.com/olekukonko/tablewriter/README.md @@ -1,11 +1,13 @@ ASCII Table Writer ========= -[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) [![Total views](https://sourcegraph.com/api/repos/github.com/olekukonko/tablewriter/counters/views.png)](https://sourcegraph.com/github.com/olekukonko/tablewriter) +[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) +[![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter) +[![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter) Generate ASCII table on the fly ... Installation is simple as - go get github.com/olekukonko/tablewriter + go get github.com/olekukonko/tablewriter #### Features @@ -22,7 +24,8 @@ Generate ASCII table on the fly ... Installation is simple as - Enable or disable table border - Set custom footer support - Optional identical cells merging - +- Set custom caption +- Optional reflowing of paragrpahs in multi-line cells. #### Example 1 - Basic ```go @@ -75,21 +78,21 @@ table.Render() ``` DATE | DESCRIPTION | CV2 | AMOUNT -+----------+--------------------------+-------+---------+ +-----------+--------------------------+-------+---------- 1/1/2014 | Domain name | 2233 | $10.98 1/1/2014 | January Hosting | 2233 | $54.95 1/4/2014 | February Hosting | 2233 | $51.00 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 -+----------+--------------------------+-------+---------+ +-----------+--------------------------+-------+---------- TOTAL | $146 93 - +-------+---------+ + --------+---------- ``` #### Example 3 - CSV ```go -table, _ := tablewriter.NewCSV(os.Stdout, "test_info.csv", true) +table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true) table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment table.Render() ``` @@ -107,12 +110,12 @@ table.Render() #### Example 4 - Custom Separator ```go -table, _ := tablewriter.NewCSV(os.Stdout, "test.csv", true) +table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true) table.SetRowLine(true) // Enable row line // Change table lines table.SetCenterSeparator("*") -table.SetColumnSeparator("‡") +table.SetColumnSeparator("╪") table.SetRowSeparator("-") table.SetAlignment(tablewriter.ALIGN_LEFT) @@ -132,7 +135,7 @@ table.Render() *------------*-----------*---------* ``` -##### Example 5 - Markdown Format +#### Example 5 - Markdown Format ```go data := [][]string{ []string{"1/1/2014", "Domain name", "2233", "$10.98"}, @@ -194,11 +197,109 @@ table.Render() +----------+--------------------------+-------+---------+ ``` + +#### Table with color +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer +table.SetBorder(false) // Set Border to false + +table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, + tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, + tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) + +table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) + +table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.FgHiRedColor}) + +table.AppendBulk(data) +table.Render() +``` + +#### Table with color Output +![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png) + +#### Example 6 - Set table caption +```go +data := [][]string{ + []string{"A", "The Good", "500"}, + []string{"B", "The Very very Bad Man", "288"}, + []string{"C", "The Ugly", "120"}, + []string{"D", "The Gopher", "800"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Sign", "Rating"}) +table.SetCaption(true, "Movie ratings.") + +for _, v := range data { + table.Append(v) +} +table.Render() // Send output +``` + +Note: Caption text will wrap with total width of rendered table. + +##### Output 6 +``` ++------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +Movie ratings. +``` + +#### Render table into a string + +Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the `strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example: + +```go +package main + +import ( + "strings" + "fmt" + + "github.com/olekukonko/tablewriter" +) + +func main() { + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + + /* + * Code to fill the table + */ + + table.Render() + + fmt.Println(tableString.String()) +} +``` + #### TODO - ~~Import Directly from CSV~~ - `done` - ~~Support for `SetFooter`~~ - `done` - ~~Support for `SetBorder`~~ - `done` - ~~Support table with uneven rows~~ - `done` -- Support custom alignment +- ~~Support custom alignment~~ - General Improvement & Optimisation - `NewHTML` Parse table from HTML diff --git a/vendor/github.com/olekukonko/tablewriter/table.go b/vendor/github.com/olekukonko/tablewriter/table.go index 3314bfba5c..3cf09969e6 100644 --- a/vendor/github.com/olekukonko/tablewriter/table.go +++ b/vendor/github.com/olekukonko/tablewriter/table.go @@ -36,8 +36,8 @@ const ( ) var ( - decimal = regexp.MustCompile(`^-*\d*\.?\d*$`) - percent = regexp.MustCompile(`^-*\d*\.?\d*$%$`) + decimal = regexp.MustCompile(`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$`) + percent = regexp.MustCompile(`^-?\d+\.?\d*$%$`) ) type Border struct { @@ -53,10 +53,13 @@ type Table struct { lines [][][]string cs map[int]int rs map[int]int - headers []string - footers []string + headers [][]string + footers [][]string + caption bool + captionText string autoFmt bool autoWrap bool + reflowText bool mW int pCenter string pRow string @@ -72,40 +75,51 @@ type Table struct { hdrLine bool borders Border colSize int + headerParams []string + columnsParams []string + footerParams []string + columnsAlign []int } // Start New Table // Take io.Writer Directly func NewWriter(writer io.Writer) *Table { t := &Table{ - out: writer, - rows: [][]string{}, - lines: [][][]string{}, - cs: make(map[int]int), - rs: make(map[int]int), - headers: []string{}, - footers: []string{}, - autoFmt: true, - autoWrap: true, - mW: MAX_ROW_WIDTH, - pCenter: CENTER, - pRow: ROW, - pColumn: COLUMN, - tColumn: -1, - tRow: -1, - hAlign: ALIGN_DEFAULT, - fAlign: ALIGN_DEFAULT, - align: ALIGN_DEFAULT, - newLine: NEWLINE, - rowLine: false, - hdrLine: true, - borders: Border{Left: true, Right: true, Bottom: true, Top: true}, - colSize: -1} + out: writer, + rows: [][]string{}, + lines: [][][]string{}, + cs: make(map[int]int), + rs: make(map[int]int), + headers: [][]string{}, + footers: [][]string{}, + caption: false, + captionText: "Table caption.", + autoFmt: true, + autoWrap: true, + reflowText: true, + mW: MAX_ROW_WIDTH, + pCenter: CENTER, + pRow: ROW, + pColumn: COLUMN, + tColumn: -1, + tRow: -1, + hAlign: ALIGN_DEFAULT, + fAlign: ALIGN_DEFAULT, + align: ALIGN_DEFAULT, + newLine: NEWLINE, + rowLine: false, + hdrLine: true, + borders: Border{Left: true, Right: true, Bottom: true, Top: true}, + colSize: -1, + headerParams: []string{}, + columnsParams: []string{}, + footerParams: []string{}, + columnsAlign: []int{}} return t } // Render table output -func (t Table) Render() { +func (t *Table) Render() { if t.borders.Top { t.printLine(true) } @@ -115,20 +129,27 @@ func (t Table) Render() { } else { t.printRows() } - if !t.rowLine && t.borders.Bottom { t.printLine(true) } t.printFooter() + if t.caption { + t.printCaption() + } } +const ( + headerRowIdx = -1 + footerRowIdx = -2 +) + // Set table header func (t *Table) SetHeader(keys []string) { t.colSize = len(keys) for i, v := range keys { - t.parseDimension(v, i, -1) - t.headers = append(t.headers, v) + lines := t.parseDimension(v, i, headerRowIdx) + t.headers = append(t.headers, lines) } } @@ -136,8 +157,16 @@ func (t *Table) SetHeader(keys []string) { func (t *Table) SetFooter(keys []string) { //t.colSize = len(keys) for i, v := range keys { - t.parseDimension(v, i, -1) - t.footers = append(t.footers, v) + lines := t.parseDimension(v, i, footerRowIdx) + t.footers = append(t.footers, lines) + } +} + +// Set table Caption +func (t *Table) SetCaption(caption bool, captionText ...string) { + t.caption = caption + if len(captionText) == 1 { + t.captionText = captionText[0] } } @@ -151,11 +180,21 @@ func (t *Table) SetAutoWrapText(auto bool) { t.autoWrap = auto } +// Turn automatic reflowing of multiline text when rewrapping. Default is on (true). +func (t *Table) SetReflowDuringAutoWrap(auto bool) { + t.reflowText = auto +} + // Set the Default column width func (t *Table) SetColWidth(width int) { t.mW = width } +// Set the minimal width for a column +func (t *Table) SetColMinWidth(column int, width int) { + t.cs[column] = width +} + // Set the Column Separator func (t *Table) SetColumnSeparator(sep string) { t.pColumn = sep @@ -186,6 +225,22 @@ func (t *Table) SetAlignment(align int) { t.align = align } +func (t *Table) SetColumnAlignment(keys []int) { + for _, v := range keys { + switch v { + case ALIGN_CENTER: + break + case ALIGN_LEFT: + break + case ALIGN_RIGHT: + break + default: + v = ALIGN_DEFAULT + } + t.columnsAlign = append(t.columnsAlign, v) + } +} + // Set New Line func (t *Table) SetNewLine(nl string) { t.newLine = nl @@ -249,16 +304,44 @@ func (t *Table) AppendBulk(rows [][]string) { } } +// NumLines to get the number of lines +func (t *Table) NumLines() int { + return len(t.lines) +} + +// Clear rows +func (t *Table) ClearRows() { + t.lines = [][][]string{} +} + +// Clear footer +func (t *Table) ClearFooter() { + t.footers = [][]string{} +} + +// Center based on position and border. +func (t *Table) center(i int) string { + if i == -1 && !t.borders.Left { + return t.pRow + } + + if i == len(t.cs)-1 && !t.borders.Right { + return t.pRow + } + + return t.pCenter +} + // Print line based on row width -func (t Table) printLine(nl bool) { - fmt.Fprint(t.out, t.pCenter) +func (t *Table) printLine(nl bool) { + fmt.Fprint(t.out, t.center(-1)) for i := 0; i < len(t.cs); i++ { v := t.cs[i] fmt.Fprintf(t.out, "%s%s%s%s", t.pRow, strings.Repeat(string(t.pRow), v), t.pRow, - t.pCenter) + t.center(i)) } if nl { fmt.Fprint(t.out, t.newLine) @@ -266,7 +349,7 @@ func (t Table) printLine(nl bool) { } // Print line based on row width with our without cell separator -func (t Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) { +func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) { fmt.Fprint(t.out, t.pCenter) for i := 0; i < len(t.cs); i++ { v := t.cs[i] @@ -303,43 +386,64 @@ func pad(align int) func(string, string, int) string { } // Print heading information -func (t Table) printHeading() { +func (t *Table) printHeading() { // Check if headers is available if len(t.headers) < 1 { return } - // Check if border is set - // Replace with space if not set - fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) - // Identify last column end := len(t.cs) - 1 // Get pad function padFunc := pad(t.hAlign) - // Print Heading column - for i := 0; i <= end; i++ { - v := t.cs[i] - h := t.headers[i] - if t.autoFmt { - h = Title(h) - } - pad := ConditionString((i == end && !t.borders.Left), SPACE, t.pColumn) - fmt.Fprintf(t.out, " %s %s", - padFunc(h, SPACE, v), - pad) + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.headerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[headerRowIdx] + + // Print Heading + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + + for y := 0; y <= end; y++ { + v := t.cs[y] + h := "" + if y < len(t.headers) && x < len(t.headers[y]) { + h = t.headers[y][x] + } + if t.autoFmt { + h = Title(h) + } + pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn) + + if is_esc_seq { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(h, SPACE, v), + t.headerParams[y]), pad) + } else { + fmt.Fprintf(t.out, " %s %s", + padFunc(h, SPACE, v), + pad) + } + } + // Next line + fmt.Fprint(t.out, t.newLine) } - // Next line - fmt.Fprint(t.out, t.newLine) if t.hdrLine { t.printLine(true) } } // Print heading information -func (t Table) printFooter() { +func (t *Table) printFooter() { // Check if headers is available if len(t.footers) < 1 { return @@ -349,9 +453,6 @@ func (t Table) printFooter() { if !t.borders.Bottom { t.printLine(true) } - // Check if border is set - // Replace with space if not set - fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE)) // Identify last column end := len(t.cs) - 1 @@ -359,25 +460,56 @@ func (t Table) printFooter() { // Get pad function padFunc := pad(t.fAlign) - // Print Heading column - for i := 0; i <= end; i++ { - v := t.cs[i] - f := t.footers[i] - if t.autoFmt { - f = Title(f) - } - pad := ConditionString((i == end && !t.borders.Top), SPACE, t.pColumn) - - if len(t.footers[i]) == 0 { - pad = SPACE - } - fmt.Fprintf(t.out, " %s %s", - padFunc(f, SPACE, v), - pad) + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.footerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[footerRowIdx] + + // Print Footer + erasePad := make([]bool, len(t.footers)) + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE)) + + for y := 0; y <= end; y++ { + v := t.cs[y] + f := "" + if y < len(t.footers) && x < len(t.footers[y]) { + f = t.footers[y][x] + } + if t.autoFmt { + f = Title(f) + } + pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn) + + if erasePad[y] || (x == 0 && len(f) == 0) { + pad = SPACE + erasePad[y] = true + } + + if is_esc_seq { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(f, SPACE, v), + t.footerParams[y]), pad) + } else { + fmt.Fprintf(t.out, " %s %s", + padFunc(f, SPACE, v), + pad) + } + + //fmt.Fprintf(t.out, " %s %s", + // padFunc(f, SPACE, v), + // pad) + } + // Next line + fmt.Fprint(t.out, t.newLine) + //t.printLine(true) } - // Next line - fmt.Fprint(t.out, t.newLine) - //t.printLine(true) hasPrinted := false @@ -385,7 +517,7 @@ func (t Table) printFooter() { v := t.cs[i] pad := t.pRow center := t.pCenter - length := len(t.footers[i]) + length := len(t.footers[i][0]) if length > 0 { hasPrinted = true @@ -398,6 +530,9 @@ func (t Table) printFooter() { // Print first junction if i == 0 { + if length > 0 && !t.borders.Left { + center = t.pRow + } fmt.Fprint(t.out, center) } @@ -405,16 +540,27 @@ func (t Table) printFooter() { if length == 0 { pad = SPACE } - // Ignore left space of it has printed before + // Ignore left space as it has printed before if hasPrinted || t.borders.Left { pad = t.pRow center = t.pCenter } + // Change Center end position + if center != SPACE { + if i == end && !t.borders.Right { + center = t.pRow + } + } + // Change Center start position if center == SPACE { - if i < end && len(t.footers[i+1]) != 0 { - center = t.pCenter + if i < end && len(t.footers[i+1][0]) != 0 { + if !t.borders.Left { + center = t.pRow + } else { + center = t.pCenter + } } } @@ -428,22 +574,53 @@ func (t Table) printFooter() { } fmt.Fprint(t.out, t.newLine) +} +// Print caption text +func (t Table) printCaption() { + width := t.getTableWidth() + paragraph, _ := WrapString(t.captionText, width) + for linecount := 0; linecount < len(paragraph); linecount++ { + fmt.Fprintln(t.out, paragraph[linecount]) + } +} + +// Calculate the total number of characters in a row +func (t Table) getTableWidth() int { + var chars int + for _, v := range t.cs { + chars += v + } + + // Add chars, spaces, seperators to calculate the total width of the table. + // ncols := t.colSize + // spaces := ncols * 2 + // seps := ncols + 1 + + return (chars + (3 * t.colSize) + 2) } func (t Table) printRows() { for i, lines := range t.lines { t.printRow(lines, i) } +} +func (t *Table) fillAlignment(num int) { + if len(t.columnsAlign) < num { + t.columnsAlign = make([]int, num) + for i := range t.columnsAlign { + t.columnsAlign[i] = t.align + } + } } // Print Row Information // Adjust column alignment based on type -func (t Table) printRow(columns [][]string, colKey int) { +func (t *Table) printRow(columns [][]string, rowIdx int) { // Get Maximum Height - max := t.rs[colKey] + max := t.rs[rowIdx] total := len(columns) // TODO Fix uneven col size @@ -455,9 +632,15 @@ func (t Table) printRow(columns [][]string, colKey int) { //} // Pad Each Height - // pads := []int{} pads := []int{} + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } + t.fillAlignment(total) + for i, line := range columns { length := len(line) pad := max - length @@ -476,9 +659,14 @@ func (t Table) printRow(columns [][]string, colKey int) { fmt.Fprintf(t.out, SPACE) str := columns[y][x] + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + // This would print alignment // Default alignment would use multiple configuration - switch t.align { + switch t.columnsAlign[y] { case ALIGN_CENTER: // fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) case ALIGN_RIGHT: @@ -514,7 +702,7 @@ func (t Table) printRow(columns [][]string, colKey int) { } // Print the rows of the table and merge the cells that are identical -func (t Table) printRowsMergeCells() { +func (t *Table) printRowsMergeCells() { var previousLine []string var displayCellBorder []bool var tmpWriter bytes.Buffer @@ -537,14 +725,19 @@ func (t Table) printRowsMergeCells() { // Print Row Information to a writer and merge identical cells. // Adjust column alignment based on type -func (t Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey int, previousLine []string) ([]string, []bool) { +func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) { // Get Maximum Height - max := t.rs[colKey] + max := t.rs[rowIdx] total := len(columns) // Pad Each Height pads := []int{} + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } for i, line := range columns { length := len(line) pad := max - length @@ -555,6 +748,7 @@ func (t Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey i } var displayCellBorder []bool + t.fillAlignment(total) for x := 0; x < max; x++ { for y := 0; y < total; y++ { @@ -565,6 +759,11 @@ func (t Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey i str := columns[y][x] + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + if t.autoMergeCells { //Store the full line to merge mutli-lines cells fullLine := strings.Join(columns[y], " ") @@ -580,7 +779,7 @@ func (t Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey i // This would print alignment // Default alignment would use multiple configuration - switch t.align { + switch t.columnsAlign[y] { case ALIGN_CENTER: // fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y])) case ALIGN_RIGHT: @@ -613,44 +812,59 @@ func (t Table) printRowMergeCells(writer io.Writer, columns [][]string, colKey i func (t *Table) parseDimension(str string, colKey, rowKey int) []string { var ( - raw []string - max int + raw []string + maxWidth int ) - w := DisplayWidth(str) - // Calculate Width - // Check if with is grater than maximum width - if w > t.mW { - w = t.mW - } - - // Check if width exists - v, ok := t.cs[colKey] - if !ok || v < w || v == 0 { - t.cs[colKey] = w - } - - if rowKey == -1 { - return raw - } - // Calculate Height - if t.autoWrap { - raw, _ = WrapString(str, t.cs[colKey]) - } else { - raw = getLines(str) - } + raw = getLines(str) + maxWidth = 0 for _, line := range raw { - if w := DisplayWidth(line); w > max { - max = w + if w := DisplayWidth(line); w > maxWidth { + maxWidth = w } } - // Make sure the with is the same length as maximum word - // Important for cases where the width is smaller than maxu word - if max > t.cs[colKey] { - t.cs[colKey] = max + // If wrapping, ensure that all paragraphs in the cell fit in the + // specified width. + if t.autoWrap { + // If there's a maximum allowed width for wrapping, use that. + if maxWidth > t.mW { + maxWidth = t.mW + } + + // In the process of doing so, we need to recompute maxWidth. This + // is because perhaps a word in the cell is longer than the + // allowed maximum width in t.mW. + newMaxWidth := maxWidth + newRaw := make([]string, 0, len(raw)) + + if t.reflowText { + // Make a single paragraph of everything. + raw = []string{strings.Join(raw, " ")} + } + for i, para := range raw { + paraLines, _ := WrapString(para, maxWidth) + for _, line := range paraLines { + if w := DisplayWidth(line); w > newMaxWidth { + newMaxWidth = w + } + } + if i > 0 { + newRaw = append(newRaw, " ") + } + newRaw = append(newRaw, paraLines...) + } + raw = newRaw + maxWidth = newMaxWidth } + // Store the new known maximum width. + v, ok := t.cs[colKey] + if !ok || v < maxWidth || v == 0 { + t.cs[colKey] = maxWidth + } + + // Remember the number of lines for the row printer. h := len(raw) v, ok = t.rs[rowKey] diff --git a/vendor/github.com/olekukonko/tablewriter/table_with_color.go b/vendor/github.com/olekukonko/tablewriter/table_with_color.go new file mode 100644 index 0000000000..5a4a53ec2a --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table_with_color.go @@ -0,0 +1,134 @@ +package tablewriter + +import ( + "fmt" + "strconv" + "strings" +) + +const ESC = "\033" +const SEP = ";" + +const ( + BgBlackColor int = iota + 40 + BgRedColor + BgGreenColor + BgYellowColor + BgBlueColor + BgMagentaColor + BgCyanColor + BgWhiteColor +) + +const ( + FgBlackColor int = iota + 30 + FgRedColor + FgGreenColor + FgYellowColor + FgBlueColor + FgMagentaColor + FgCyanColor + FgWhiteColor +) + +const ( + BgHiBlackColor int = iota + 100 + BgHiRedColor + BgHiGreenColor + BgHiYellowColor + BgHiBlueColor + BgHiMagentaColor + BgHiCyanColor + BgHiWhiteColor +) + +const ( + FgHiBlackColor int = iota + 90 + FgHiRedColor + FgHiGreenColor + FgHiYellowColor + FgHiBlueColor + FgHiMagentaColor + FgHiCyanColor + FgHiWhiteColor +) + +const ( + Normal = 0 + Bold = 1 + UnderlineSingle = 4 + Italic +) + +type Colors []int + +func startFormat(seq string) string { + return fmt.Sprintf("%s[%sm", ESC, seq) +} + +func stopFormat() string { + return fmt.Sprintf("%s[%dm", ESC, Normal) +} + +// Making the SGR (Select Graphic Rendition) sequence. +func makeSequence(codes []int) string { + codesInString := []string{} + for _, code := range codes { + codesInString = append(codesInString, strconv.Itoa(code)) + } + return strings.Join(codesInString, SEP) +} + +// Adding ANSI escape sequences before and after string +func format(s string, codes interface{}) string { + var seq string + + switch v := codes.(type) { + + case string: + seq = v + case []int: + seq = makeSequence(v) + default: + return s + } + + if len(seq) == 0 { + return s + } + return startFormat(seq) + s + stopFormat() +} + +// Adding header colors (ANSI codes) +func (t *Table) SetHeaderColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of header colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.headerParams = append(t.headerParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetColumnColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of column colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.columnsParams = append(t.columnsParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetFooterColor(colors ...Colors) { + if len(t.footers) != len(colors) { + panic("Number of footer colors must be equal to number of footer.") + } + for i := 0; i < len(colors); i++ { + t.footerParams = append(t.footerParams, makeSequence(colors[i])) + } +} + +func Color(colors ...int) []int { + return colors +} diff --git a/vendor/github.com/olekukonko/tablewriter/test.csv b/vendor/github.com/olekukonko/tablewriter/test.csv deleted file mode 100644 index 1609327e93..0000000000 --- a/vendor/github.com/olekukonko/tablewriter/test.csv +++ /dev/null @@ -1,4 +0,0 @@ -first_name,last_name,ssn -John,Barry,123456 -Kathy,Smith,687987 -Bob,McCornick,3979870 \ No newline at end of file diff --git a/vendor/github.com/olekukonko/tablewriter/test_info.csv b/vendor/github.com/olekukonko/tablewriter/test_info.csv deleted file mode 100644 index e4c40e983a..0000000000 --- a/vendor/github.com/olekukonko/tablewriter/test_info.csv +++ /dev/null @@ -1,4 +0,0 @@ -Field,Type,Null,Key,Default,Extra -user_id,smallint(5),NO,PRI,NULL,auto_increment -username,varchar(10),NO,,NULL, -password,varchar(100),NO,,NULL, \ No newline at end of file diff --git a/vendor/github.com/olekukonko/tablewriter/util.go b/vendor/github.com/olekukonko/tablewriter/util.go index 2deefbc52a..380e7ab35b 100644 --- a/vendor/github.com/olekukonko/tablewriter/util.go +++ b/vendor/github.com/olekukonko/tablewriter/util.go @@ -30,17 +30,38 @@ func ConditionString(cond bool, valid, inValid string) string { return inValid } +func isNumOrSpace(r rune) bool { + return ('0' <= r && r <= '9') || r == ' ' +} + // Format Table Header // Replace _ , . and spaces func Title(name string) string { - name = strings.Replace(name, "_", " ", -1) - name = strings.Replace(name, ".", " ", -1) + origLen := len(name) + rs := []rune(name) + for i, r := range rs { + switch r { + case '_': + rs[i] = ' ' + case '.': + // ignore floating number 0.0 + if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) { + rs[i] = ' ' + } + } + } + name = string(rs) name = strings.TrimSpace(name) + if len(name) == 0 && origLen > 0 { + // Keep at least one character. This is important to preserve + // empty lines in multi-line headers/footers. + name = " " + } return strings.ToUpper(name) } // Pad String -// Attempts to play string in the center +// Attempts to place string in the center func Pad(s, pad string, width int) string { gap := width - DisplayWidth(s) if gap > 0 { @@ -52,7 +73,7 @@ func Pad(s, pad string, width int) string { } // Pad String Right position -// This would pace string at the left side fo the screen +// This would place string at the left side of the screen func PadRight(s, pad string, width int) string { gap := width - DisplayWidth(s) if gap > 0 { @@ -62,7 +83,7 @@ func PadRight(s, pad string, width int) string { } // Pad String Left position -// This would pace string at the right side fo the screen +// This would place string at the right side of the screen func PadLeft(s, pad string, width int) string { gap := width - DisplayWidth(s) if gap > 0 { diff --git a/vendor/github.com/olekukonko/tablewriter/wrap.go b/vendor/github.com/olekukonko/tablewriter/wrap.go index 5290fb65a2..a092ee1f75 100644 --- a/vendor/github.com/olekukonko/tablewriter/wrap.go +++ b/vendor/github.com/olekukonko/tablewriter/wrap.go @@ -10,7 +10,8 @@ package tablewriter import ( "math" "strings" - "unicode/utf8" + + "github.com/mattn/go-runewidth" ) var ( @@ -27,7 +28,7 @@ func WrapString(s string, lim int) ([]string, int) { var lines []string max := 0 for _, v := range words { - max = len(v) + max = runewidth.StringWidth(v) if max > lim { lim = max } @@ -55,9 +56,9 @@ func WrapWords(words []string, spc, lim, pen int) [][]string { length := make([][]int, n) for i := 0; i < n; i++ { length[i] = make([]int, n) - length[i][i] = utf8.RuneCountInString(words[i]) + length[i][i] = runewidth.StringWidth(words[i]) for j := i + 1; j < n; j++ { - length[i][j] = length[i][j-1] + spc + utf8.RuneCountInString(words[j]) + length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j]) } } nbrk := make([]int, n) @@ -94,10 +95,5 @@ func WrapWords(words []string, spc, lim, pen int) [][]string { // getLines decomposes a multiline string into a slice of strings. func getLines(s string) []string { - var lines []string - - for _, line := range strings.Split(s, nl) { - lines = append(lines, line) - } - return lines + return strings.Split(s, nl) } diff --git a/vendor/vendor.json b/vendor/vendor.json index c760d2cb77..406c1c4c7a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -311,10 +311,10 @@ "revisionTime": "2017-04-03T15:03:10Z" }, { - "checksumSHA1": "h+oCMj21PiQfIdBog0eyUtF1djs=", + "checksumSHA1": "HZJ2dhzXoMi8n+iY80A9vsnyQUk=", "path": "github.com/olekukonko/tablewriter", - "revision": "febf2d34b54a69ce7530036c7503b1c9fbfdf0bb", - "revisionTime": "2017-01-28T05:05:32Z" + "revision": "7e037d187b0c13d81ccf0dd1c6b990c2759e6597", + "revisionTime": "2019-04-09T13:48:02Z" }, { "checksumSHA1": "a/DHmc9bdsYlZZcwp6i3xhvV7Pk=", From 536b3b416c6ff53ea11a0d29dcc351a6d7919901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Wed, 15 May 2019 14:33:33 +0300 Subject: [PATCH 25/34] cosensus, core, eth, params, trie: fixes + clique history cap --- consensus/clique/clique.go | 7 ++-- consensus/clique/snapshot.go | 16 ++++++++- consensus/ethash/sealer.go | 4 +-- core/blockchain.go | 17 ++++++++- core/rawdb/database.go | 59 ++++++++++++++++++++++--------- core/rawdb/freezer.go | 14 +++----- core/rawdb/freezer_table_test.go | 2 +- eth/downloader/downloader.go | 32 ++++++++--------- eth/downloader/downloader_test.go | 2 +- eth/downloader/testchain_test.go | 2 +- params/network_params.go | 6 ++++ trie/sync_bloom.go | 4 +-- 12 files changed, 112 insertions(+), 53 deletions(-) diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 4caa9dc74d..084009a066 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -365,8 +365,11 @@ func (c *Clique) snapshot(chain consensus.ChainReader, number uint64, hash commo break } } - // If we're at an checkpoint block, make a snapshot if it's known - if number == 0 || (number%c.config.Epoch == 0 && chain.GetHeaderByNumber(number-1) == nil) { + // If we're at the genesis, snapshot the initial state. Alternatively if we're + // at a checkpoint block without a parent (light client CHT), or we have piled + // up more headers than allowed to be reorged (chain reinit from a freezer), + // consider the checkpoint trusted and snapshot it. + if number == 0 || (number%c.config.Epoch == 0 && (len(headers) > params.ImmutabilityThreshold || chain.GetHeaderByNumber(number-1) == nil)) { checkpoint := chain.GetHeaderByNumber(number) if checkpoint != nil { hash := checkpoint.Hash() diff --git a/consensus/clique/snapshot.go b/consensus/clique/snapshot.go index 54d4555f3b..4ee731a908 100644 --- a/consensus/clique/snapshot.go +++ b/consensus/clique/snapshot.go @@ -20,10 +20,12 @@ import ( "bytes" "encoding/json" "sort" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" lru "github.com/hashicorp/golang-lru" ) @@ -197,7 +199,11 @@ func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) { // Iterate through the headers and create a new snapshot snap := s.copy() - for _, header := range headers { + var ( + start = time.Now() + logged = time.Now() + ) + for i, header := range headers { // Remove any votes on checkpoint blocks number := header.Number.Uint64() if number%s.config.Epoch == 0 { @@ -285,6 +291,14 @@ func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) { } delete(snap.Tally, header.Coinbase) } + // If we're taking too much time (ecrecover), notify the user once a while + if time.Since(logged) > 8*time.Second { + log.Info("Reconstructing voting history", "processed", i, "total", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + if time.Since(start) > 8*time.Second { + log.Info("Reconstructed voting history", "processed", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) } snap.Number += uint64(len(headers)) snap.Hash = headers[len(headers)-1].Hash() diff --git a/consensus/ethash/sealer.go b/consensus/ethash/sealer.go index 3a0919ca99..43db1fcb7f 100644 --- a/consensus/ethash/sealer.go +++ b/consensus/ethash/sealer.go @@ -270,7 +270,7 @@ func (ethash *Ethash) remote(notify []string, noverify bool) { start := time.Now() if !noverify { if err := ethash.verifySeal(nil, header, true); err != nil { - log.Warn("Invalid proof-of-work submitted", "sealhash", sealhash, "elapsed", time.Since(start), "err", err) + log.Warn("Invalid proof-of-work submitted", "sealhash", sealhash, "elapsed", common.PrettyDuration(time.Since(start)), "err", err) return false } } @@ -279,7 +279,7 @@ func (ethash *Ethash) remote(notify []string, noverify bool) { log.Warn("Ethash result channel is empty, submitted mining result is rejected") return false } - log.Trace("Verified correct proof-of-work", "sealhash", sealhash, "elapsed", time.Since(start)) + log.Trace("Verified correct proof-of-work", "sealhash", sealhash, "elapsed", common.PrettyDuration(time.Since(start))) // Solutions seems to be valid, return to the miner and notify acceptance. solution := block.WithSeal(header) diff --git a/core/blockchain.go b/core/blockchain.go index 651c67c5d6..7ab6806c2f 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -221,6 +221,10 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par // Initialize the chain with ancient data if it isn't empty. if bc.empty() { if frozen, err := bc.db.Ancients(); err == nil && frozen > 0 { + var ( + start = time.Now() + logged time.Time + ) for i := uint64(0); i < frozen; i++ { // Inject hash<->number mapping. hash := rawdb.ReadCanonicalHash(bc.db, i) @@ -235,12 +239,23 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par return nil, errors.New("broken ancient database") } rawdb.WriteTxLookupEntries(bc.db, block) + + // If we've spent too much time already, notify the user of what we're doing + if time.Since(logged) > 8*time.Second { + log.Info("Initializing chain from ancient data", "number", i, "hash", hash, "total", frozen-1, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } } hash := rawdb.ReadCanonicalHash(bc.db, frozen-1) rawdb.WriteHeadHeaderHash(bc.db, hash) rawdb.WriteHeadFastBlockHash(bc.db, hash) - log.Info("Initialized chain with ancients", "number", frozen-1, "hash", hash) + // The first thing the node will do is reconstruct the verification data for + // the head block (ethash cache or clique voting snapshot). Might as well do + // it in advance. + bc.engine.VerifyHeader(bc, rawdb.ReadHeader(bc.db, hash, frozen-1), true) + + log.Info("Initialized chain from ancient data", "number", frozen-1, "hash", hash, "elapsed", common.PrettyDuration(time.Since(start))) } } if err := bc.loadLastState(); err != nil { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 016c6c9094..37379147cd 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -179,17 +179,18 @@ func InspectDatabase(db ethdb.Database) error { logged = time.Now() // Key-value store statistics - total common.StorageSize - headerSize common.StorageSize - bodySize common.StorageSize - receiptSize common.StorageSize - tdSize common.StorageSize - numHashPairing common.StorageSize - hashNumPairing common.StorageSize - trieSize common.StorageSize - txlookupSize common.StorageSize - preimageSize common.StorageSize - bloomBitsSize common.StorageSize + total common.StorageSize + headerSize common.StorageSize + bodySize common.StorageSize + receiptSize common.StorageSize + tdSize common.StorageSize + numHashPairing common.StorageSize + hashNumPairing common.StorageSize + trieSize common.StorageSize + txlookupSize common.StorageSize + preimageSize common.StorageSize + bloomBitsSize common.StorageSize + cliqueSnapsSize common.StorageSize // Ancient store statistics ancientHeaders common.StorageSize @@ -199,8 +200,12 @@ func InspectDatabase(db ethdb.Database) error { ancientTds common.StorageSize // Les statistic - ChtTrieNodes common.StorageSize - BloomTrieNodes common.StorageSize + chtTrieNodes common.StorageSize + bloomTrieNodes common.StorageSize + + // Meta- and unaccounted data + metadata common.StorageSize + unaccounted common.StorageSize ) // Inspect key-value database first. for it.Next() { @@ -228,12 +233,26 @@ func InspectDatabase(db ethdb.Database) error { preimageSize += size case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength): bloomBitsSize += size + case bytes.HasPrefix(key, []byte("clique-")) && len(key) == 7+common.HashLength: + cliqueSnapsSize += size case bytes.HasPrefix(key, []byte("cht-")) && len(key) == 4+common.HashLength: - ChtTrieNodes += size + chtTrieNodes += size case bytes.HasPrefix(key, []byte("blt-")) && len(key) == 4+common.HashLength: - BloomTrieNodes += size + bloomTrieNodes += size case len(key) == common.HashLength: trieSize += size + default: + var accounted bool + for _, meta := range [][]byte{databaseVerisionKey, headHeaderKey, headBlockKey, headFastBlockKey, fastTrieProgressKey, ancientKey} { + if bytes.Equal(key, meta) { + metadata += size + accounted = true + break + } + } + if !accounted { + unaccounted += size + } } count += 1 if count%1000 == 0 && time.Since(logged) > 8*time.Second { @@ -261,18 +280,24 @@ func InspectDatabase(db ethdb.Database) error { {"Key-Value store", "Bloombit index", bloomBitsSize.String()}, {"Key-Value store", "Trie nodes", trieSize.String()}, {"Key-Value store", "Trie preimages", preimageSize.String()}, + {"Key-Value store", "Clique snapshots", cliqueSnapsSize.String()}, + {"Key-Value store", "Singleton metadata", metadata.String()}, {"Ancient store", "Headers", ancientHeaders.String()}, {"Ancient store", "Bodies", ancientBodies.String()}, {"Ancient store", "Receipts", ancientReceipts.String()}, {"Ancient store", "Difficulties", ancientTds.String()}, {"Ancient store", "Block number->hash", ancientHashes.String()}, - {"Light client", "CHT trie nodes", ChtTrieNodes.String()}, - {"Light client", "Bloom trie nodes", BloomTrieNodes.String()}, + {"Light client", "CHT trie nodes", chtTrieNodes.String()}, + {"Light client", "Bloom trie nodes", bloomTrieNodes.String()}, } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"Database", "Category", "Size"}) table.SetFooter([]string{"", "Total", total.String()}) table.AppendBulk(stats) table.Render() + + if unaccounted > 0 { + log.Error("Database contains unaccounted data", "size", unaccounted) + } return nil } diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index f3a6bbb8f9..67ed87d66c 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/params" "github.com/prometheus/tsdb/fileutil" ) @@ -52,11 +53,6 @@ const ( // storage. freezerRecheckInterval = time.Minute - // freezerBlockGraduation is the number of confirmations a block must achieve - // before it becomes elligible for chain freezing. This must exceed any chain - // reorg depth, since the freezer also deletes all block siblings. - freezerBlockGraduation = 90000 - // freezerBatchLimit is the maximum number of blocks to freeze in one batch // before doing an fsync and deleting it from the key-value store. freezerBatchLimit = 30000 @@ -268,12 +264,12 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) { time.Sleep(freezerRecheckInterval) continue - case *number < freezerBlockGraduation: - log.Debug("Current full block not old enough", "number", *number, "hash", hash, "delay", freezerBlockGraduation) + case *number < params.ImmutabilityThreshold: + log.Debug("Current full block not old enough", "number", *number, "hash", hash, "delay", params.ImmutabilityThreshold) time.Sleep(freezerRecheckInterval) continue - case *number-freezerBlockGraduation <= f.frozen: + case *number-params.ImmutabilityThreshold <= f.frozen: log.Debug("Ancient blocks frozen already", "number", *number, "hash", hash, "frozen", f.frozen) time.Sleep(freezerRecheckInterval) continue @@ -285,7 +281,7 @@ func (f *freezer) freeze(db ethdb.KeyValueStore) { continue } // Seems we have data ready to be frozen, process in usable batches - limit := *number - freezerBlockGraduation + limit := *number - params.ImmutabilityThreshold if limit-f.frozen > freezerBatchLimit { limit = f.frozen + freezerBatchLimit } diff --git a/core/rawdb/freezer_table_test.go b/core/rawdb/freezer_table_test.go index 9a7eec5054..e63fb63a3c 100644 --- a/core/rawdb/freezer_table_test.go +++ b/core/rawdb/freezer_table_test.go @@ -35,7 +35,7 @@ func init() { // Gets a chunk of data, filled with 'b' func getChunk(size int, b int) []byte { data := make([]byte, size) - for i, _ := range data { + for i := range data { data[i] = byte(b) } return data diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 5c350debea..495fa0e74d 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -25,7 +25,7 @@ import ( "sync/atomic" "time" - ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" @@ -46,20 +46,20 @@ var ( MaxReceiptFetch = 256 // Amount of transaction receipts to allow fetching per request MaxStateFetch = 384 // Amount of node state values to allow fetching per request - MaxForkAncestry = 3 * params.EpochDuration // Maximum chain reorganisation - rttMinEstimate = 2 * time.Second // Minimum round-trip time to target for download requests - rttMaxEstimate = 20 * time.Second // Maximum round-trip time to target for download requests - rttMinConfidence = 0.1 // Worse confidence factor in our estimated RTT value - ttlScaling = 3 // Constant scaling factor for RTT -> TTL conversion - ttlLimit = time.Minute // Maximum TTL allowance to prevent reaching crazy timeouts + rttMinEstimate = 2 * time.Second // Minimum round-trip time to target for download requests + rttMaxEstimate = 20 * time.Second // Maximum round-trip time to target for download requests + rttMinConfidence = 0.1 // Worse confidence factor in our estimated RTT value + ttlScaling = 3 // Constant scaling factor for RTT -> TTL conversion + ttlLimit = time.Minute // Maximum TTL allowance to prevent reaching crazy timeouts qosTuningPeers = 5 // Number of peers to tune based on (best peers) qosConfidenceCap = 10 // Number of peers above which not to modify RTT confidence qosTuningImpact = 0.25 // Impact that a new tuning target has on the previous value - maxQueuedHeaders = 32 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) - maxHeadersProcess = 2048 // Number of header download results to import at once into the chain - maxResultsProcess = 2048 // Number of content download results to import at once into the chain + maxQueuedHeaders = 32 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) + maxHeadersProcess = 2048 // Number of header download results to import at once into the chain + maxResultsProcess = 2048 // Number of content download results to import at once into the chain + maxForkAncestry uint64 = params.ImmutabilityThreshold // Maximum chain reorganisation (locally redeclared so tests can reduce it) reorgProtThreshold = 48 // Threshold number of recent blocks to disable mini reorg protection reorgProtHeaderDelay = 2 // Number of headers to delay delivering to cover mini reorgs @@ -439,7 +439,7 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.I log.Debug("Synchronising with the network", "peer", p.id, "eth", p.version, "head", hash, "td", td, "mode", d.mode) defer func(start time.Time) { - log.Debug("Synchronisation terminated", "elapsed", time.Since(start)) + log.Debug("Synchronisation terminated", "elapsed", common.PrettyDuration(time.Since(start))) }(time.Now()) // Look up the sync boundaries: the common ancestor and the target block @@ -491,10 +491,10 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.I // The peer would start to feed us valid blocks until head, resulting in all of // the blocks might be written into the ancient store. A following mini-reorg // could cause issues. - if d.checkpoint != 0 && d.checkpoint > MaxForkAncestry+1 { + if d.checkpoint != 0 && d.checkpoint > maxForkAncestry+1 { d.ancientLimit = d.checkpoint - } else if height > MaxForkAncestry+1 { - d.ancientLimit = height - MaxForkAncestry - 1 + } else if height > maxForkAncestry+1 { + d.ancientLimit = height - maxForkAncestry - 1 } frozen, _ := d.stateDB.Ancients() // Ignore the error here since light client can also hit here. // If a part of blockchain data has already been written into active store, @@ -725,9 +725,9 @@ func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) p.log.Debug("Looking for common ancestor", "local", localHeight, "remote", remoteHeight) // Recap floor value for binary search - if localHeight >= MaxForkAncestry { + if localHeight >= maxForkAncestry { // We're above the max reorg threshold, find the earliest fork point - floor = int64(localHeight - MaxForkAncestry) + floor = int64(localHeight - maxForkAncestry) } // If we're doing a light sync, ensure the floor doesn't go below the CHT, as // all headers before that point will be missing. diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 659aee0881..5b56ff161a 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -37,7 +37,7 @@ import ( // Reduce some of the parameters to make the tester faster. func init() { - MaxForkAncestry = uint64(10000) + maxForkAncestry = 10000 blockCacheItems = 1024 fsHeaderContCheck = 500 * time.Millisecond } diff --git a/eth/downloader/testchain_test.go b/eth/downloader/testchain_test.go index 4ae342dc63..f410152f5b 100644 --- a/eth/downloader/testchain_test.go +++ b/eth/downloader/testchain_test.go @@ -45,7 +45,7 @@ var testChainBase = newTestChain(blockCacheItems+200, testGenesis) var testChainForkLightA, testChainForkLightB, testChainForkHeavy *testChain func init() { - var forkLen = int(MaxForkAncestry + 50) + var forkLen = int(maxForkAncestry + 50) var wg sync.WaitGroup wg.Add(3) go func() { testChainForkLightA = testChainBase.makeFork(forkLen, false, 1); wg.Done() }() diff --git a/params/network_params.go b/params/network_params.go index 024c4af457..a949b84578 100644 --- a/params/network_params.go +++ b/params/network_params.go @@ -46,4 +46,10 @@ const ( // HelperTrieProcessConfirmations is the number of confirmations before a HelperTrie // is generated HelperTrieProcessConfirmations = 256 + + // ImmutabilityThreshold is the number of blocks after which a chain segment is + // considered immutable (i.e. soft finality). It is used by the downloader as a + // hard limit against deep ancestors, by the blockchain against deep reorgs, by + // the freezer as the cutoff treshold and by clique as the snapshot trust limit. + ImmutabilityThreshold = 90000 ) diff --git a/trie/sync_bloom.go b/trie/sync_bloom.go index 899a63add8..7b5b7488a6 100644 --- a/trie/sync_bloom.go +++ b/trie/sync_bloom.go @@ -118,14 +118,14 @@ func (b *SyncBloom) init(database ethdb.Iteratee) { it.Release() it = database.NewIteratorWithStart(key) - log.Info("Initializing fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate(), "elapsed", time.Since(start)) + log.Info("Initializing fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate(), "elapsed", common.PrettyDuration(time.Since(start))) swap = time.Now() } } it.Release() // Mark the bloom filter inited and return - log.Info("Initialized fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate(), "elapsed", time.Since(start)) + log.Info("Initialized fast sync bloom", "items", b.bloom.N(), "errorrate", b.errorRate(), "elapsed", common.PrettyDuration(time.Since(start))) atomic.StoreUint32(&b.inited, 1) } From 1e067202a2311e583238026da0c9032e28a3f0b2 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Thu, 16 May 2019 15:47:11 +0200 Subject: [PATCH 26/34] swarm/feeds: Parallel feed lookups (#19414) --- swarm/storage/feed/cacheentry.go | 2 +- swarm/storage/feed/handler.go | 21 +- swarm/storage/feed/handler_test.go | 8 +- swarm/storage/feed/id_test.go | 4 +- .../feed/lookup/algorithm_fluzcapacitor.go | 63 ++ .../feed/lookup/algorithm_longearth.go | 185 ++++++ swarm/storage/feed/lookup/epoch.go | 2 +- swarm/storage/feed/lookup/lookup.go | 75 +-- swarm/storage/feed/lookup/lookup_test.go | 614 +++++++++++------- swarm/storage/feed/lookup/store_test.go | 154 +++++ swarm/storage/feed/lookup/timesim_test.go | 128 ++++ swarm/storage/feed/query_test.go | 2 +- swarm/storage/feed/request_test.go | 2 +- swarm/storage/feed/update_test.go | 2 +- 14 files changed, 936 insertions(+), 326 deletions(-) create mode 100644 swarm/storage/feed/lookup/algorithm_fluzcapacitor.go create mode 100644 swarm/storage/feed/lookup/algorithm_longearth.go create mode 100644 swarm/storage/feed/lookup/store_test.go create mode 100644 swarm/storage/feed/lookup/timesim_test.go diff --git a/swarm/storage/feed/cacheentry.go b/swarm/storage/feed/cacheentry.go index be42008e99..1c7e226198 100644 --- a/swarm/storage/feed/cacheentry.go +++ b/swarm/storage/feed/cacheentry.go @@ -27,7 +27,7 @@ import ( const ( hasherCount = 8 feedsHashAlgorithm = storage.SHA3Hash - defaultRetrieveTimeout = 100 * time.Millisecond + defaultRetrieveTimeout = 1000 * time.Millisecond ) // cacheEntry caches the last known update of a specific Swarm feed. diff --git a/swarm/storage/feed/handler.go b/swarm/storage/feed/handler.go index 0f6f2ba340..98ed7fa991 100644 --- a/swarm/storage/feed/handler.go +++ b/swarm/storage/feed/handler.go @@ -23,6 +23,7 @@ import ( "context" "fmt" "sync" + "sync/atomic" "github.com/ethereum/go-ethereum/swarm/chunk" @@ -178,12 +179,12 @@ func (h *Handler) Lookup(ctx context.Context, query *Query) (*cacheEntry, error) return nil, NewError(ErrInit, "Call Handler.SetStore() before performing lookups") } - var readCount int + var readCount int32 // Invoke the lookup engine. // The callback will be called every time the lookup algorithm needs to guess requestPtr, err := lookup.Lookup(ctx, timeLimit, query.Hint, func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { - readCount++ + atomic.AddInt32(&readCount, 1) id := ID{ Feed: query.Feed, Epoch: epoch, @@ -228,17 +229,17 @@ func (h *Handler) updateCache(request *Request) (*cacheEntry, error) { updateAddr := request.Addr() log.Trace("feed cache update", "topic", request.Topic.Hex(), "updateaddr", updateAddr, "epoch time", request.Epoch.Time, "epoch level", request.Epoch.Level) - feedUpdate := h.get(&request.Feed) - if feedUpdate == nil { - feedUpdate = &cacheEntry{} - h.set(&request.Feed, feedUpdate) + entry := h.get(&request.Feed) + if entry == nil { + entry = &cacheEntry{} + h.set(&request.Feed, entry) } // update our rsrcs entry map - feedUpdate.lastKey = updateAddr - feedUpdate.Update = request.Update - feedUpdate.Reader = bytes.NewReader(feedUpdate.data) - return feedUpdate, nil + entry.lastKey = updateAddr + entry.Update = request.Update + entry.Reader = bytes.NewReader(entry.data) + return entry, nil } // Update publishes a feed update diff --git a/swarm/storage/feed/handler_test.go b/swarm/storage/feed/handler_test.go index c4f6fe689e..3d8213e60b 100644 --- a/swarm/storage/feed/handler_test.go +++ b/swarm/storage/feed/handler_test.go @@ -177,8 +177,8 @@ func TestFeedsHandler(t *testing.T) { if err != nil { t.Fatal(err) } - if request.Epoch.Base() != 0 || request.Epoch.Level != 22 { - t.Fatalf("Expected epoch base time to be %d, got %d. Expected epoch level to be %d, got %d", 0, request.Epoch.Base(), 22, request.Epoch.Level) + if request.Epoch.Base() != 0 || request.Epoch.Level != 28 { + t.Fatalf("Expected epoch base time to be %d, got %d. Expected epoch level to be %d, got %d", 0, request.Epoch.Base(), 28, request.Epoch.Level) } data = []byte(updates[3]) request.SetData(data) @@ -213,8 +213,8 @@ func TestFeedsHandler(t *testing.T) { if !bytes.Equal(update2.data, []byte(updates[len(updates)-1])) { t.Fatalf("feed update data was %v, expected %v", string(update2.data), updates[len(updates)-1]) } - if update2.Level != 22 { - t.Fatalf("feed update epoch level was %d, expected 22", update2.Level) + if update2.Level != 28 { + t.Fatalf("feed update epoch level was %d, expected 28", update2.Level) } if update2.Base() != 0 { t.Fatalf("feed update epoch base time was %d, expected 0", update2.Base()) diff --git a/swarm/storage/feed/id_test.go b/swarm/storage/feed/id_test.go index e561ff9b46..8a820abfed 100644 --- a/swarm/storage/feed/id_test.go +++ b/swarm/storage/feed/id_test.go @@ -16,11 +16,11 @@ func getTestID() *ID { func TestIDAddr(t *testing.T) { id := getTestID() updateAddr := id.Addr() - compareByteSliceToExpectedHex(t, "updateAddr", updateAddr, "0x8b24583ec293e085f4c78aaee66d1bc5abfb8b4233304d14a349afa57af2a783") + compareByteSliceToExpectedHex(t, "updateAddr", updateAddr, "0x842d0a81987b9755dfeaa5558f5c134c1c0af48b6545005cac7b533d9411453a") } func TestIDSerializer(t *testing.T) { - testBinarySerializerRecovery(t, getTestID(), "0x776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce803000000000019") + testBinarySerializerRecovery(t, getTestID(), "0x776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce80300000000001f") } func TestIDLengthCheck(t *testing.T) { diff --git a/swarm/storage/feed/lookup/algorithm_fluzcapacitor.go b/swarm/storage/feed/lookup/algorithm_fluzcapacitor.go new file mode 100644 index 0000000000..3840bd0fd6 --- /dev/null +++ b/swarm/storage/feed/lookup/algorithm_fluzcapacitor.go @@ -0,0 +1,63 @@ +package lookup + +import "context" + +// FluzCapacitorAlgorithm works by narrowing the epoch search area if an update is found +// going back and forth in time +// First, it will attempt to find an update where it should be now if the hint was +// really the last update. If that lookup fails, then the last update must be either the hint itself +// or the epochs right below. If however, that lookup succeeds, then the update must be +// that one or within the epochs right below. +// see the guide for a more graphical representation +func FluzCapacitorAlgorithm(ctx context.Context, now uint64, hint Epoch, read ReadFunc) (value interface{}, err error) { + var lastFound interface{} + var epoch Epoch + if hint == NoClue { + hint = worstHint + } + + t := now + + for { + epoch = GetNextEpoch(hint, t) + value, err = read(ctx, epoch, now) + if err != nil { + return nil, err + } + if value != nil { + lastFound = value + if epoch.Level == LowestLevel || epoch.Equals(hint) { + return value, nil + } + hint = epoch + continue + } + if epoch.Base() == hint.Base() { + if lastFound != nil { + return lastFound, nil + } + // we have reached the hint itself + if hint == worstHint { + return nil, nil + } + // check it out + value, err = read(ctx, hint, now) + if err != nil { + return nil, err + } + if value != nil { + return value, nil + } + // bad hint. + t = hint.Base() + hint = worstHint + continue + } + base := epoch.Base() + if base == 0 { + return nil, nil + } + t = base - 1 + } + +} diff --git a/swarm/storage/feed/lookup/algorithm_longearth.go b/swarm/storage/feed/lookup/algorithm_longearth.go new file mode 100644 index 0000000000..d0342f67cb --- /dev/null +++ b/swarm/storage/feed/lookup/algorithm_longearth.go @@ -0,0 +1,185 @@ +package lookup + +import ( + "context" + "sync/atomic" + "time" +) + +type stepFunc func(ctx context.Context, t uint64, hint Epoch) interface{} + +// LongEarthLookaheadDelay is the headstart the lookahead gives R before it launches +var LongEarthLookaheadDelay = 250 * time.Millisecond + +// LongEarthLookbackDelay is the headstart the lookback gives R before it launches +var LongEarthLookbackDelay = 250 * time.Millisecond + +// LongEarthAlgorithm explores possible lookup paths in parallel, pruning paths as soon +// as a more promising lookup path is found. As a result, this lookup algorithm is an order +// of magnitude faster than the FluzCapacitor algorithm, but at the expense of more exploratory reads. +// This algorithm works as follows. On each step, the next epoch is immediately looked up (R) +// and given a head start, while two parallel "steps" are launched a short time after: +// look ahead (A) is the path the algorithm would take if the R lookup returns a value, whereas +// look back (B) is the path the algorithm would take if the R lookup failed. +// as soon as R is actually finished, the A or B paths are pruned depending on the value of R. +// if A returns earlier than R, then R and B read operations can be safely canceled, saving time. +// The maximum number of active read operations is calculated as 2^(timeout/headstart). +// If headstart is infinite, this algorithm behaves as FluzCapacitor. +// timeout is the maximum execution time of the passed `read` function. +// the two head starts can be configured by changing LongEarthLookaheadDelay or LongEarthLookbackDelay +func LongEarthAlgorithm(ctx context.Context, now uint64, hint Epoch, read ReadFunc) (interface{}, error) { + if hint == NoClue { + hint = worstHint + } + + var stepCounter int32 // for debugging, stepCounter allows to give an ID to each step instance + + errc := make(chan struct{}) // errc will help as an error shortcut signal + var gerr error // in case of error, this variable will be set + + var step stepFunc // For efficiency, the algorithm step is defined as a closure + step = func(ctxS context.Context, t uint64, last Epoch) interface{} { + stepID := atomic.AddInt32(&stepCounter, 1) // give an ID to this call instance + trace(stepID, "init: t=%d, last=%s", t, last.String()) + var valueA, valueB, valueR interface{} + + // initialize the three read contexts + ctxR, cancelR := context.WithCancel(ctxS) // will handle the current read operation + ctxA, cancelA := context.WithCancel(ctxS) // will handle the lookahead path + ctxB, cancelB := context.WithCancel(ctxS) // will handle the lookback path + + epoch := GetNextEpoch(last, t) // calculate the epoch to look up in this step instance + + // define the lookAhead function, which will follow the path as if R was successful + lookAhead := func() { + valueA = step(ctxA, t, epoch) // launch the next step, recursively. + if valueA != nil { // if this path is successful, we don't need R or B. + cancelB() + cancelR() + } + } + + // define the lookBack function, which will follow the path as if R was unsuccessful + lookBack := func() { + if epoch.Base() == last.Base() { + return + } + base := epoch.Base() + if base == 0 { + return + } + valueB = step(ctxB, base-1, last) + } + + go func() { //goroutine to read the current epoch (R) + defer cancelR() + var err error + valueR, err = read(ctxR, epoch, now) // read this epoch + if valueR == nil { // if unsuccessful, cancel lookahead, otherwise cancel lookback. + cancelA() + } else { + cancelB() + } + if err != nil && err != context.Canceled { + gerr = err + close(errc) + } + }() + + go func() { // goroutine to give a headstart to R and then launch lookahead. + defer cancelA() + + // if we are at the lowest level or the epoch to look up equals the last one, + // then we cannot lookahead (can't go lower or repeat the same lookup, this would + // cause an infinite loop) + if epoch.Level == LowestLevel || epoch.Equals(last) { + return + } + + // give a head start to R, or launch immediately if R finishes early enough + select { + case <-TimeAfter(LongEarthLookaheadDelay): + lookAhead() + case <-ctxR.Done(): + if valueR != nil { + lookAhead() // only look ahead if R was successful + } + case <-ctxA.Done(): + } + }() + + go func() { // goroutine to give a headstart to R and then launch lookback. + defer cancelB() + + // give a head start to R, or launch immediately if R finishes early enough + select { + case <-TimeAfter(LongEarthLookbackDelay): + lookBack() + case <-ctxR.Done(): + if valueR == nil { + lookBack() // only look back in case R failed + } + case <-ctxB.Done(): + } + }() + + <-ctxA.Done() + if valueA != nil { + trace(stepID, "Returning valueA=%v", valueA) + return valueA + } + + <-ctxR.Done() + if valueR != nil { + trace(stepID, "Returning valueR=%v", valueR) + return valueR + } + <-ctxB.Done() + trace(stepID, "Returning valueB=%v", valueB) + return valueB + } + + var value interface{} + stepCtx, cancel := context.WithCancel(ctx) + + go func() { // launch the root step in its own goroutine to allow cancellation + defer cancel() + value = step(stepCtx, now, hint) + }() + + // wait for the algorithm to finish, but shortcut in case + // of errors + select { + case <-stepCtx.Done(): + case <-errc: + cancel() + return nil, gerr + } + + if ctx.Err() != nil { + return nil, ctx.Err() + } + + if value != nil || hint == worstHint { + return value, nil + } + + // at this point the algorithm did not return a value, + // so we challenge the hint given. + value, err := read(ctx, hint, now) + if err != nil { + return nil, err + } + if value != nil { + return value, nil // hint is valid, return it. + } + + // hint is invalid. Invoke the algorithm + // without hint. + now = hint.Base() + if hint.Level == HighestLevel { + now-- + } + + return LongEarthAlgorithm(ctx, now, NoClue, read) +} diff --git a/swarm/storage/feed/lookup/epoch.go b/swarm/storage/feed/lookup/epoch.go index bafe954779..6d75ba2432 100644 --- a/swarm/storage/feed/lookup/epoch.go +++ b/swarm/storage/feed/lookup/epoch.go @@ -87,5 +87,5 @@ func (e *Epoch) Equals(epoch Epoch) bool { // String implements the Stringer interface. func (e *Epoch) String() string { - return fmt.Sprintf("Epoch{Time:%d, Level:%d}", e.Time, e.Level) + return fmt.Sprintf("Epoch{Base: %d, Time:%d, Level:%d}", e.Base(), e.Time, e.Level) } diff --git a/swarm/storage/feed/lookup/lookup.go b/swarm/storage/feed/lookup/lookup.go index 1642c659a8..4b233a0e07 100644 --- a/swarm/storage/feed/lookup/lookup.go +++ b/swarm/storage/feed/lookup/lookup.go @@ -20,7 +20,10 @@ so they can be found */ package lookup -import "context" +import ( + "context" + "time" +) const maxuint64 = ^uint64(0) @@ -28,8 +31,8 @@ const maxuint64 = ^uint64(0) const LowestLevel uint8 = 0 // default is 0 (1 second) // HighestLevel sets the lowest frequency the algorithm will operate at, as a power of 2. -// 25 -> 2^25 equals to roughly one year. -const HighestLevel = 25 // default is 25 (~1 year) +// 31 -> 2^31 equals to roughly 38 years. +const HighestLevel = 31 // DefaultLevel sets what level will be chosen to search when there is no hint const DefaultLevel = HighestLevel @@ -43,7 +46,12 @@ type Algorithm func(ctx context.Context, now uint64, hint Epoch, read ReadFunc) // read() will be called on each lookup attempt // Returns an error only if read() returns an error // Returns nil if an update was not found -var Lookup Algorithm = FluzCapacitorAlgorithm +var Lookup Algorithm = LongEarthAlgorithm + +// TimeAfter must point to a function that returns a timer +// This is here so that tests can replace it with +// a mock up timer factory to simulate time deterministically +var TimeAfter = time.After // ReadFunc is a handler called by Lookup each time it attempts to find a value // It should return if a value is not found @@ -123,61 +131,6 @@ func GetFirstEpoch(now uint64) Epoch { var worstHint = Epoch{Time: 0, Level: 63} -// FluzCapacitorAlgorithm works by narrowing the epoch search area if an update is found -// going back and forth in time -// First, it will attempt to find an update where it should be now if the hint was -// really the last update. If that lookup fails, then the last update must be either the hint itself -// or the epochs right below. If however, that lookup succeeds, then the update must be -// that one or within the epochs right below. -// see the guide for a more graphical representation -func FluzCapacitorAlgorithm(ctx context.Context, now uint64, hint Epoch, read ReadFunc) (value interface{}, err error) { - var lastFound interface{} - var epoch Epoch - if hint == NoClue { - hint = worstHint - } - - t := now - - for { - epoch = GetNextEpoch(hint, t) - value, err = read(ctx, epoch, now) - if err != nil { - return nil, err - } - if value != nil { - lastFound = value - if epoch.Level == LowestLevel || epoch.Equals(hint) { - return value, nil - } - hint = epoch - continue - } - if epoch.Base() == hint.Base() { - if lastFound != nil { - return lastFound, nil - } - // we have reached the hint itself - if hint == worstHint { - return nil, nil - } - // check it out - value, err = read(ctx, hint, now) - if err != nil { - return nil, err - } - if value != nil { - return value, nil - } - // bad hint. - t = hint.Base() - hint = worstHint - continue - } - base := epoch.Base() - if base == 0 { - return nil, nil - } - t = base - 1 - } +var trace = func(id int32, formatString string, a ...interface{}) { + //fmt.Printf("Step ID #%d "+formatString+"\n", append([]interface{}{id}, a...)...) } diff --git a/swarm/storage/feed/lookup/lookup_test.go b/swarm/storage/feed/lookup/lookup_test.go index 60d77b7096..b5d1a3b0ba 100644 --- a/swarm/storage/feed/lookup/lookup_test.go +++ b/swarm/storage/feed/lookup/lookup_test.go @@ -21,56 +21,55 @@ import ( "fmt" "math/rand" "testing" + "time" - "github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/storage/feed/lookup" ) -type Data struct { - Payload uint64 - Time uint64 +type AlgorithmInfo struct { + Lookup lookup.Algorithm + Name string } -type Store map[lookup.EpochID]*Data - -func write(store Store, epoch lookup.Epoch, value *Data) { - log.Debug("Write: %d-%d, value='%d'\n", epoch.Base(), epoch.Level, value.Payload) - store[epoch.ID()] = value +var algorithms = []AlgorithmInfo{ + {lookup.FluzCapacitorAlgorithm, "FluzCapacitor"}, + {lookup.LongEarthAlgorithm, "LongEarth"}, } -func update(store Store, last lookup.Epoch, now uint64, value *Data) lookup.Epoch { - epoch := lookup.GetNextEpoch(last, now) +const enablePrintMetrics = false // set to true to display algorithm benchmarking stats - write(store, epoch, value) - - return epoch +func printMetric(metric string, store *Store, elapsed time.Duration) { + if enablePrintMetrics { + fmt.Printf("metric=%s, readcount=%d (successful=%d, failed=%d), cached=%d, canceled=%d, maxSimult=%d, elapsed=%s\n", metric, + store.reads, store.successful, store.failed, store.cacheHits, store.canceled, store.maxSimultaneous, elapsed) + } } const Day = 60 * 60 * 24 const Year = Day * 365 const Month = Day * 30 -func makeReadFunc(store Store, counter *int) lookup.ReadFunc { - return func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { - *counter++ - data := store[epoch.ID()] - var valueStr string - if data != nil { - valueStr = fmt.Sprintf("%d", data.Payload) - } - log.Debug("Read: %d-%d, value='%s'\n", epoch.Base(), epoch.Level, valueStr) - if data != nil && data.Time <= now { - return data, nil - } - return nil, nil - } +// DefaultStoreConfig indicates the time the different read +// operations will take in the simulation +// This allows to measure an algorithm performance relative +// to other +var DefaultStoreConfig = &StoreConfig{ + CacheReadTime: 50 * time.Millisecond, + FailedReadTime: 1000 * time.Millisecond, + SuccessfulReadTime: 500 * time.Millisecond, } +// TestLookup verifies if the last update and intermediates are +// found and if that same last update is found faster if a hint is given func TestLookup(t *testing.T) { + // ### 1.- Initialize stopwatch time sim + stopwatch := NewStopwatch(50 * time.Millisecond) + lookup.TimeAfter = stopwatch.TimeAfter() + defer stopwatch.Stop() - store := make(Store) - readCount := 0 - readFunc := makeReadFunc(store, &readCount) + // ### 2.- Setup mock storage and generate updates + store := NewStore(DefaultStoreConfig) + readFunc := store.MakeReadFunc() // write an update every month for 12 months 3 years ago and then silence for two years now := uint64(1533799046) @@ -83,68 +82,90 @@ func TestLookup(t *testing.T) { Payload: t, //our "payload" will be the timestamp itself. Time: t, } - epoch = update(store, epoch, t, &data) + epoch = store.Update(epoch, t, &data) lastData = &data } - // try to get the last value + // ### 3.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { - value, err := lookup.Lookup(context.Background(), now, lookup.NoClue, readFunc) - if err != nil { - t.Fatal(err) + store.Reset() // reset the store read counters + + // ### 3.1.- Test how long it takes to find the last update without a hint: + timeElapsedWithoutHint := stopwatch.Measure(func() { + + // try to get the last value + value, err := algo.Lookup(context.Background(), now, lookup.NoClue, readFunc) + if err != nil { + t.Fatal(err) + } + if value != lastData { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) + } + + }) + printMetric("SIMPLE READ", store, timeElapsedWithoutHint) + + store.Reset() // reset the read counters for the next test + + // ### 3.2.- Test how long it takes to find the last update *with* a hint. + // it should take less time! + timeElapsed := stopwatch.Measure(func() { + // Provide a hint to get a faster lookup. In particular, we give the exact location of the last update + value, err := algo.Lookup(context.Background(), now, epoch, readFunc) + if err != nil { + t.Fatal(err) + } + if value != lastData { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) + } + }) + printMetric("WITH HINT", store, stopwatch.Elapsed()) + + if timeElapsed > timeElapsedWithoutHint { + t.Fatalf("Expected lookup to complete faster than %s since we provided a hint. Took %s", timeElapsedWithoutHint, timeElapsed) + } + + store.Reset() // reset the read counters for the next test + + // ### 3.3.- try to get an intermediate value + // if we look for a value in, e.g., now - Year*3 + 6*Month, we should get that value + // Since the "payload" is the timestamp itself, we can check this. + expectedTime := now - Year*3 + 6*Month + timeElapsed = stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), expectedTime, lookup.NoClue, readFunc) + if err != nil { + t.Fatal(err) + } + + data, ok := value.(*Data) + + if !ok { + t.Fatal("Expected value to contain data") + } + + if data.Time != expectedTime { + t.Fatalf("Expected value timestamp to be %d, got %d", data.Time, expectedTime) + } + }) + printMetric("INTERMEDIATE READ", store, timeElapsed) + }) } - - readCountWithoutHint := readCount - - if value != lastData { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) - } - - // reset the read count for the next test - readCount = 0 - // Provide a hint to get a faster lookup. In particular, we give the exact location of the last update - value, err = lookup.Lookup(context.Background(), now, epoch, readFunc) - if err != nil { - t.Fatal(err) - } - - if value != lastData { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) - } - - if readCount > readCountWithoutHint { - t.Fatalf("Expected lookup to complete with fewer or same reads than %d since we provided a hint. Did %d reads.", readCountWithoutHint, readCount) - } - - // try to get an intermediate value - // if we look for a value in now - Year*3 + 6*Month, we should get that value - // Since the "payload" is the timestamp itself, we can check this. - - expectedTime := now - Year*3 + 6*Month - - value, err = lookup.Lookup(context.Background(), expectedTime, lookup.NoClue, readFunc) - if err != nil { - t.Fatal(err) - } - - data, ok := value.(*Data) - - if !ok { - t.Fatal("Expected value to contain data") - } - - if data.Time != expectedTime { - t.Fatalf("Expected value timestamp to be %d, got %d", data.Time, expectedTime) - } - } +// TestOneUpdateAt0 checks if the lookup algorithm can return an update that +// is precisely set at t=0 func TestOneUpdateAt0(t *testing.T) { + // ### 1.- Initialize stopwatch time sim + stopwatch := NewStopwatch(50 * time.Millisecond) + lookup.TimeAfter = stopwatch.TimeAfter() + defer stopwatch.Stop() - store := make(Store) - readCount := 0 + // ### 2.- Setup mock storage and generate updates + store := NewStore(DefaultStoreConfig) + readFunc := store.MakeReadFunc() - readFunc := makeReadFunc(store, &readCount) now := uint64(1533903729) var epoch lookup.Epoch @@ -152,24 +173,37 @@ func TestOneUpdateAt0(t *testing.T) { Payload: 79, Time: 0, } - update(store, epoch, 0, &data) + store.Update(epoch, 0, &data) //place 1 update in t=0 - value, err := lookup.Lookup(context.Background(), now, lookup.NoClue, readFunc) - if err != nil { - t.Fatal(err) - } - if value != &data { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", data, value) + // ### 3.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { + store.Reset() // reset the read counters for the next test + timeElapsed := stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), now, lookup.NoClue, readFunc) + if err != nil { + t.Fatal(err) + } + if value != &data { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", data, value) + } + }) + printMetric("SIMPLE", store, timeElapsed) + }) } } -// Tests the update is found even when a bad hint is given +// TestBadHint tests if the update is found even when a bad hint is given func TestBadHint(t *testing.T) { + // ### 1.- Initialize stopwatch time sim + stopwatch := NewStopwatch(50 * time.Millisecond) + lookup.TimeAfter = stopwatch.TimeAfter() + defer stopwatch.Stop() - store := make(Store) - readCount := 0 + // ### 2.- Setup mock storage and generate updates + store := NewStore(DefaultStoreConfig) + readFunc := store.MakeReadFunc() - readFunc := makeReadFunc(store, &readCount) now := uint64(1533903729) var epoch lookup.Epoch @@ -179,7 +213,7 @@ func TestBadHint(t *testing.T) { } // place an update for t=1200 - update(store, epoch, 1200, &data) + store.Update(epoch, 1200, &data) // come up with some evil hint badHint := lookup.Epoch{ @@ -187,21 +221,35 @@ func TestBadHint(t *testing.T) { Time: 1200000000, } - value, err := lookup.Lookup(context.Background(), now, badHint, readFunc) - if err != nil { - t.Fatal(err) - } - if value != &data { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", data, value) + // ### 3.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { + store.Reset() + timeElapsed := stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), now, badHint, readFunc) + if err != nil { + t.Fatal(err) + } + if value != &data { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", data, value) + } + }) + printMetric("SIMPLE", store, timeElapsed) + }) } } -// Tests whether the update is found when the bad hint is exactly below the last update +// TestBadHintNextToUpdate checks whether the update is found when the bad hint is exactly below the last update func TestBadHintNextToUpdate(t *testing.T) { - store := make(Store) - readCount := 0 + // ### 1.- Initialize stopwatch time sim + stopwatch := NewStopwatch(50 * time.Millisecond) + lookup.TimeAfter = stopwatch.TimeAfter() + defer stopwatch.Stop() + + // ### 2.- Setup mock storage and generate updates + store := NewStore(DefaultStoreConfig) + readFunc := store.MakeReadFunc() - readFunc := makeReadFunc(store, &readCount) now := uint64(1533903729) var last *Data @@ -227,7 +275,7 @@ func TestBadHintNextToUpdate(t *testing.T) { Time: 0, } last = &data - epoch = update(store, epoch, 1200000000+i, &data) + epoch = store.Update(epoch, 1200000000+i, &data) } // come up with some evil hint: @@ -237,99 +285,132 @@ func TestBadHintNextToUpdate(t *testing.T) { Time: 1200000005, } - value, err := lookup.Lookup(context.Background(), now, badHint, readFunc) - if err != nil { - t.Fatal(err) - } - if value != last { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", last, value) + // ### 3.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { + store.Reset() // reset read counters for next test + + timeElapsed := stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), now, badHint, readFunc) + if err != nil { + t.Fatal(err) + } + if value != last { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", last, value) + } + }) + printMetric("SIMPLE", store, timeElapsed) + }) } } +// TestContextCancellation checks whether a lookup can be canceled func TestContextCancellation(t *testing.T) { - readFunc := func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { - <-ctx.Done() - return nil, ctx.Err() - } + // ### 1.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + // ### 2.1.- Test a simple cancel of an always blocking read function + readFunc := func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { + <-ctx.Done() + return nil, ctx.Err() + } - errc := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + errc := make(chan error) - go func() { - _, err := lookup.Lookup(ctx, 1200000000, lookup.NoClue, readFunc) - errc <- err - }() + go func() { + _, err := algo.Lookup(ctx, 1200000000, lookup.NoClue, readFunc) + errc <- err + }() - cancel() + cancel() //actually cancel the lookup - if err := <-errc; err != context.Canceled { - t.Fatalf("Expected lookup to return a context Cancelled error, got %v", err) - } + if err := <-errc; err != context.Canceled { + t.Fatalf("Expected lookup to return a context canceled error, got %v", err) + } - // text context cancellation during hint lookup: - ctx, cancel = context.WithCancel(context.Background()) - errc = make(chan error) - someHint := lookup.Epoch{ - Level: 25, - Time: 300, - } + // ### 2.2.- Test context cancellation during hint lookup: + ctx, cancel = context.WithCancel(context.Background()) + errc = make(chan error) + someHint := lookup.Epoch{ + Level: 25, + Time: 300, + } + // put up a read function that gets canceled only on hint lookup + readFunc = func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { + if epoch == someHint { + go cancel() + <-ctx.Done() + return nil, ctx.Err() + } + return nil, nil + } - readFunc = func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { - if epoch == someHint { - go cancel() - <-ctx.Done() - return nil, ctx.Err() - } - return nil, nil - } + go func() { + _, err := algo.Lookup(ctx, 301, someHint, readFunc) + errc <- err + }() - go func() { - _, err := lookup.Lookup(ctx, 301, someHint, readFunc) - errc <- err - }() - - if err := <-errc; err != context.Canceled { - t.Fatalf("Expected lookup to return a context Cancelled error, got %v", err) + if err := <-errc; err != context.Canceled { + t.Fatalf("Expected lookup to return a context canceled error, got %v", err) + } + }) } } +// TestLookupFail makes sure the lookup function fails on a timely manner +// when there are no updates at all func TestLookupFail(t *testing.T) { + // ### 1.- Initialize stopwatch time sim + stopwatch := NewStopwatch(50 * time.Millisecond) + lookup.TimeAfter = stopwatch.TimeAfter() + defer stopwatch.Stop() - store := make(Store) - readCount := 0 + // ### 2.- Setup mock storage, without adding updates + // don't write anything and try to look up. + // we're testing we don't get stuck in a loop and that the lookup + // function converges in a timely fashion + store := NewStore(DefaultStoreConfig) + readFunc := store.MakeReadFunc() - readFunc := makeReadFunc(store, &readCount) now := uint64(1533903729) - // don't write anything and try to look up. - // we're testing we don't get stuck in a loop + // ### 3.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { + store.Reset() - value, err := lookup.Lookup(context.Background(), now, lookup.NoClue, readFunc) - if err != nil { - t.Fatal(err) - } - if value != nil { - t.Fatal("Expected value to be nil, since the update should've failed") - } + stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), now, lookup.NoClue, readFunc) + if err != nil { + t.Fatal(err) + } + if value != nil { + t.Fatal("Expected value to be nil, since the update should've failed") + } + }) - expectedReads := now/(1< readCountWithoutHint { - t.Fatalf("Expected lookup to complete with fewer or equal reads than %d since we provided a hint. Did %d reads.", readCountWithoutHint, readCount) - } + // ### 3.2.- Now test how long it takes to find the last update *with* a hint, + // it should take less time! + timeElapsed := stopwatch.Measure(func() { + // Provide a hint to get a faster lookup. In particular, we give the exact location of the last update + value, err := algo.Lookup(context.Background(), now, epoch, readFunc) + stopwatch.Stop() + if err != nil { + t.Fatal(err) + } - for i := uint64(0); i <= 994; i++ { - T := uint64(now - 1000 + i) // update every second for the last 1000 seconds - value, err := lookup.Lookup(context.Background(), T, lookup.NoClue, readFunc) - if err != nil { - t.Fatal(err) - } - data, _ := value.(*Data) - if data == nil { - t.Fatalf("Expected lookup to return %d, got nil", T) - } - if data.Payload != T { - t.Fatalf("Expected lookup to return %d, got %d", T, data.Time) - } + if value != lastData { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) + } + + }) + if timeElapsed > timeElapsedWithoutHint { + t.Fatalf("Expected lookup to complete faster than %s since we provided a hint. Took %s", timeElapsedWithoutHint, timeElapsed) + } + printMetric("WITH HINT", store, timeElapsed) + + store.Reset() // reset read counters + + // ### 3.3.- Test multiple lookups at different intervals + timeElapsed = stopwatch.Measure(func() { + for i := uint64(0); i <= 10; i++ { + T := uint64(now - 1000 + i) + value, err := algo.Lookup(context.Background(), T, lookup.NoClue, readFunc) + if err != nil { + t.Fatal(err) + } + data, _ := value.(*Data) + if data == nil { + t.Fatalf("Expected lookup to return %d, got nil", T) + } + if data.Payload != T { + t.Fatalf("Expected lookup to return %d, got %d", T, data.Time) + } + } + }) + printMetric("MULTIPLE", store, timeElapsed) + }) } } +// TestSparseUpdates checks the lookup algorithm when +// updates come sparsely and in bursts func TestSparseUpdates(t *testing.T) { + // ### 1.- Initialize stopwatch time sim + stopwatch := NewStopwatch(50 * time.Millisecond) + lookup.TimeAfter = stopwatch.TimeAfter() + defer stopwatch.Stop() - store := make(Store) - readCount := 0 - readFunc := makeReadFunc(store, &readCount) + // ### 2.- Setup mock storage and write an updates sparsely in bursts, + // every 5 years 3 times starting in Jan 1st 1970 and then silence + store := NewStore(DefaultStoreConfig) + readFunc := store.MakeReadFunc() - // write an update every 5 years 3 times starting in Jan 1st 1970 and then silence - - now := uint64(1533799046) + now := uint64(633799046) var epoch lookup.Epoch var lastData *Data - for i := uint64(0); i < 5; i++ { - T := uint64(Year * 5 * i) // write an update every 5 years 3 times starting in Jan 1st 1970 and then silence - data := Data{ - Payload: T, //our "payload" will be the timestamp itself. - Time: T, + for i := uint64(0); i < 3; i++ { + for j := uint64(0); j < 10; j++ { + T := uint64(Year*5*i + j) // write a burst of 10 updates every 5 years 3 times starting in Jan 1st 1970 and then silence + data := Data{ + Payload: T, //our "payload" will be the timestamp itself. + Time: T, + } + epoch = store.Update(epoch, T, &data) + lastData = &data } - epoch = update(store, epoch, T, &data) - lastData = &data } - // try to get the last value + // ### 3.- Test all algorithms + for _, algo := range algorithms { + t.Run(algo.Name, func(t *testing.T) { + store.Reset() // reset read counters for next test - value, err := lookup.Lookup(context.Background(), now, lookup.NoClue, readFunc) - if err != nil { - t.Fatal(err) + // ### 3.1.- Test how long it takes to find the last update without a hint: + timeElapsedWithoutHint := stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), now, lookup.NoClue, readFunc) + stopwatch.Stop() + if err != nil { + t.Fatal(err) + } + + if value != lastData { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) + } + }) + printMetric("SIMPLE", store, timeElapsedWithoutHint) + + // reset the read count for the next test + store.Reset() + + // ### 3.2.- Now test how long it takes to find the last update *with* a hint, + // it should take less time! + timeElapsed := stopwatch.Measure(func() { + value, err := algo.Lookup(context.Background(), now, epoch, readFunc) + if err != nil { + t.Fatal(err) + } + + if value != lastData { + t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) + } + }) + if timeElapsed > timeElapsedWithoutHint { + t.Fatalf("Expected lookup to complete faster than %s since we provided a hint. Took %s", timeElapsedWithoutHint, timeElapsed) + } + + printMetric("WITH HINT", store, stopwatch.Elapsed()) + + }) } - - readCountWithoutHint := readCount - - if value != lastData { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) - } - - // reset the read count for the next test - readCount = 0 - // Provide a hint to get a faster lookup. In particular, we give the exact location of the last update - value, err = lookup.Lookup(context.Background(), now, epoch, readFunc) - if err != nil { - t.Fatal(err) - } - - if value != lastData { - t.Fatalf("Expected lookup to return the last written value: %v. Got %v", lastData, value) - } - - if readCount > readCountWithoutHint { - t.Fatalf("Expected lookup to complete with fewer reads than %d since we provided a hint. Did %d reads.", readCountWithoutHint, readCount) - } - } // testG will hold precooked test results @@ -447,8 +572,9 @@ type testG struct { } // test cases -var testGetNextLevelCases = []testG{{e: lookup.Epoch{Time: 989875233, Level: 12}, n: 989875233, x: 11}, {e: lookup.Epoch{Time: 995807650, Level: 18}, n: 995598156, x: 19}, {e: lookup.Epoch{Time: 969167082, Level: 0}, n: 968990357, x: 18}, {e: lookup.Epoch{Time: 993087628, Level: 14}, n: 992987044, x: 20}, {e: lookup.Epoch{Time: 963364631, Level: 20}, n: 963364630, x: 19}, {e: lookup.Epoch{Time: 963497510, Level: 16}, n: 963370732, x: 18}, {e: lookup.Epoch{Time: 955421349, Level: 22}, n: 955421348, x: 21}, {e: lookup.Epoch{Time: 968220379, Level: 15}, n: 968220378, x: 14}, {e: lookup.Epoch{Time: 939129014, Level: 6}, n: 939128771, x: 11}, {e: lookup.Epoch{Time: 907847903, Level: 6}, n: 907791833, x: 18}, {e: lookup.Epoch{Time: 910835564, Level: 15}, n: 910835564, x: 14}, {e: lookup.Epoch{Time: 913578333, Level: 22}, n: 881808431, x: 25}, {e: lookup.Epoch{Time: 895818460, Level: 3}, n: 895818132, x: 9}, {e: lookup.Epoch{Time: 903843025, Level: 24}, n: 895609561, x: 23}, {e: lookup.Epoch{Time: 877889433, Level: 13}, n: 877877093, x: 15}, {e: lookup.Epoch{Time: 901450396, Level: 10}, n: 901450058, x: 9}, {e: lookup.Epoch{Time: 925179910, Level: 3}, n: 925168393, x: 16}, {e: lookup.Epoch{Time: 913485477, Level: 21}, n: 913485476, x: 20}, {e: lookup.Epoch{Time: 924462991, Level: 18}, n: 924462990, x: 17}, {e: lookup.Epoch{Time: 941175128, Level: 13}, n: 941175127, x: 12}, {e: lookup.Epoch{Time: 920126583, Level: 3}, n: 920100782, x: 19}, {e: lookup.Epoch{Time: 932403200, Level: 9}, n: 932279891, x: 17}, {e: lookup.Epoch{Time: 948284931, Level: 2}, n: 948284921, x: 9}, {e: lookup.Epoch{Time: 953540997, Level: 7}, n: 950547986, x: 22}, {e: lookup.Epoch{Time: 926639837, Level: 18}, n: 918608882, x: 24}, {e: lookup.Epoch{Time: 954637598, Level: 1}, n: 954578761, x: 17}, {e: lookup.Epoch{Time: 943482981, Level: 10}, n: 942924151, x: 19}, {e: lookup.Epoch{Time: 963580771, Level: 7}, n: 963580771, x: 6}, {e: lookup.Epoch{Time: 993744930, Level: 7}, n: 993690858, x: 16}, {e: lookup.Epoch{Time: 1018890213, Level: 12}, n: 1018890212, x: 11}, {e: lookup.Epoch{Time: 1030309411, Level: 2}, n: 1030309227, x: 9}, {e: lookup.Epoch{Time: 1063204997, Level: 20}, n: 1063204996, x: 19}, {e: lookup.Epoch{Time: 1094340832, Level: 6}, n: 1094340633, x: 7}, {e: lookup.Epoch{Time: 1077880597, Level: 10}, n: 1075914292, x: 20}, {e: lookup.Epoch{Time: 1051114957, Level: 18}, n: 1051114957, x: 17}, {e: lookup.Epoch{Time: 1045649701, Level: 22}, n: 1045649700, x: 21}, {e: lookup.Epoch{Time: 1066198885, Level: 14}, n: 1066198884, x: 13}, {e: lookup.Epoch{Time: 1053231952, Level: 1}, n: 1053210845, x: 16}, {e: lookup.Epoch{Time: 1068763404, Level: 14}, n: 1068675428, x: 18}, {e: lookup.Epoch{Time: 1039042173, Level: 15}, n: 1038973110, x: 17}, {e: lookup.Epoch{Time: 1050747636, Level: 6}, n: 1050747364, x: 9}, {e: lookup.Epoch{Time: 1030034434, Level: 23}, n: 1030034433, x: 22}, {e: lookup.Epoch{Time: 1003783425, Level: 18}, n: 1003783424, x: 17}, {e: lookup.Epoch{Time: 988163976, Level: 15}, n: 988084064, x: 17}, {e: lookup.Epoch{Time: 1007222377, Level: 15}, n: 1007222377, x: 14}, {e: lookup.Epoch{Time: 1001211375, Level: 13}, n: 1001208178, x: 14}, {e: lookup.Epoch{Time: 997623199, Level: 8}, n: 997623198, x: 7}, {e: lookup.Epoch{Time: 1026283830, Level: 10}, n: 1006681704, x: 24}, {e: lookup.Epoch{Time: 1019421907, Level: 20}, n: 1019421906, x: 19}, {e: lookup.Epoch{Time: 1043154306, Level: 16}, n: 1043108343, x: 16}, {e: lookup.Epoch{Time: 1075643767, Level: 17}, n: 1075325898, x: 18}, {e: lookup.Epoch{Time: 1043726309, Level: 20}, n: 1043726308, x: 19}, {e: lookup.Epoch{Time: 1056415324, Level: 17}, n: 1056415324, x: 16}, {e: lookup.Epoch{Time: 1088650219, Level: 13}, n: 1088650218, x: 12}, {e: lookup.Epoch{Time: 1088551662, Level: 7}, n: 1088543355, x: 13}, {e: lookup.Epoch{Time: 1069667265, Level: 6}, n: 1069667075, x: 7}, {e: lookup.Epoch{Time: 1079145970, Level: 18}, n: 1079145969, x: 17}, {e: lookup.Epoch{Time: 1083338876, Level: 7}, n: 1083338875, x: 6}, {e: lookup.Epoch{Time: 1051581086, Level: 4}, n: 1051568869, x: 14}, {e: lookup.Epoch{Time: 1028430882, Level: 4}, n: 1028430864, x: 5}, {e: lookup.Epoch{Time: 1057356462, Level: 1}, n: 1057356417, x: 5}, {e: lookup.Epoch{Time: 1033104266, Level: 0}, n: 1033097479, x: 13}, {e: lookup.Epoch{Time: 1031391367, Level: 11}, n: 1031387304, x: 14}, {e: lookup.Epoch{Time: 1049781164, Level: 15}, n: 1049781163, x: 14}, {e: lookup.Epoch{Time: 1027271628, Level: 12}, n: 1027271627, x: 11}, {e: lookup.Epoch{Time: 1057270560, Level: 23}, n: 1057270560, x: 22}, {e: lookup.Epoch{Time: 1047501317, Level: 15}, n: 1047501317, x: 14}, {e: lookup.Epoch{Time: 1058349035, Level: 11}, n: 1045175573, x: 24}, {e: lookup.Epoch{Time: 1057396147, Level: 20}, n: 1057396147, x: 19}, {e: lookup.Epoch{Time: 1048906375, Level: 18}, n: 1039616919, x: 25}, {e: lookup.Epoch{Time: 1074294831, Level: 20}, n: 1074294831, x: 19}, {e: lookup.Epoch{Time: 1088946052, Level: 1}, n: 1088917364, x: 14}, {e: lookup.Epoch{Time: 1112337595, Level: 17}, n: 1111008110, x: 22}, {e: lookup.Epoch{Time: 1099990284, Level: 5}, n: 1099968370, x: 15}, {e: lookup.Epoch{Time: 1087036441, Level: 16}, n: 1053967855, x: 25}, {e: lookup.Epoch{Time: 1069225185, Level: 8}, n: 1069224660, x: 10}, {e: lookup.Epoch{Time: 1057505479, Level: 9}, n: 1057505170, x: 14}, {e: lookup.Epoch{Time: 1072381377, Level: 12}, n: 1065950959, x: 22}, {e: lookup.Epoch{Time: 1093887139, Level: 8}, n: 1093863305, x: 14}, {e: lookup.Epoch{Time: 1082366510, Level: 24}, n: 1082366510, x: 23}, {e: lookup.Epoch{Time: 1103231132, Level: 14}, n: 1102292201, x: 22}, {e: lookup.Epoch{Time: 1094502355, Level: 3}, n: 1094324652, x: 18}, {e: lookup.Epoch{Time: 1068488344, Level: 12}, n: 1067577330, x: 19}, {e: lookup.Epoch{Time: 1050278233, Level: 12}, n: 1050278232, x: 11}, {e: lookup.Epoch{Time: 1047660768, Level: 5}, n: 1047652137, x: 17}, {e: lookup.Epoch{Time: 1060116167, Level: 11}, n: 1060114091, x: 12}, {e: lookup.Epoch{Time: 1068149392, Level: 21}, n: 1052074801, x: 24}, {e: lookup.Epoch{Time: 1081934120, Level: 6}, n: 1081933847, x: 8}, {e: lookup.Epoch{Time: 1107943693, Level: 16}, n: 1107096139, x: 25}, {e: lookup.Epoch{Time: 1131571649, Level: 9}, n: 1131570428, x: 11}, {e: lookup.Epoch{Time: 1123139367, Level: 0}, n: 1122912198, x: 20}, {e: lookup.Epoch{Time: 1121144423, Level: 6}, n: 1120568289, x: 20}, {e: lookup.Epoch{Time: 1089932411, Level: 17}, n: 1089932410, x: 16}, {e: lookup.Epoch{Time: 1104899012, Level: 22}, n: 1098978789, x: 22}, {e: lookup.Epoch{Time: 1094588059, Level: 21}, n: 1094588059, x: 20}, {e: lookup.Epoch{Time: 1114987438, Level: 24}, n: 1114987437, x: 23}, {e: lookup.Epoch{Time: 1084186305, Level: 7}, n: 1084186241, x: 6}, {e: lookup.Epoch{Time: 1058827111, Level: 8}, n: 1058826504, x: 9}, {e: lookup.Epoch{Time: 1090679810, Level: 12}, n: 1090616539, x: 17}, {e: lookup.Epoch{Time: 1084299475, Level: 23}, n: 1084299475, x: 22}} +var testGetNextLevelCases = []testG{testG{e: lookup.Epoch{Time: 989875233, Level: 12}, n: 989807323, x: 24}, testG{e: lookup.Epoch{Time: 995807650, Level: 18}, n: 995807649, x: 17}, testG{e: lookup.Epoch{Time: 969167082, Level: 0}, n: 969111431, x: 18}, testG{e: lookup.Epoch{Time: 993087628, Level: 14}, n: 993087627, x: 13}, testG{e: lookup.Epoch{Time: 963364631, Level: 20}, n: 962941578, x: 19}, testG{e: lookup.Epoch{Time: 963497510, Level: 16}, n: 963497509, x: 15}, testG{e: lookup.Epoch{Time: 955421349, Level: 22}, n: 929292183, x: 27}, testG{e: lookup.Epoch{Time: 968220379, Level: 15}, n: 968220378, x: 14}, testG{e: lookup.Epoch{Time: 939129014, Level: 6}, n: 939126953, x: 11}, testG{e: lookup.Epoch{Time: 907847903, Level: 6}, n: 907846146, x: 11}, testG{e: lookup.Epoch{Time: 910835564, Level: 15}, n: 703619757, x: 28}, testG{e: lookup.Epoch{Time: 913578333, Level: 22}, n: 913578332, x: 21}, testG{e: lookup.Epoch{Time: 895818460, Level: 3}, n: 895818132, x: 9}, testG{e: lookup.Epoch{Time: 903843025, Level: 24}, n: 903843025, x: 23}, testG{e: lookup.Epoch{Time: 877889433, Level: 13}, n: 149120378, x: 29}, testG{e: lookup.Epoch{Time: 901450396, Level: 10}, n: 858997793, x: 26}, testG{e: lookup.Epoch{Time: 925179910, Level: 3}, n: 925177237, x: 13}, testG{e: lookup.Epoch{Time: 913485477, Level: 21}, n: 907146511, x: 22}, testG{e: lookup.Epoch{Time: 924462991, Level: 18}, n: 924462990, x: 17}, testG{e: lookup.Epoch{Time: 941175128, Level: 13}, n: 941168924, x: 13}, testG{e: lookup.Epoch{Time: 920126583, Level: 3}, n: 538054817, x: 28}, testG{e: lookup.Epoch{Time: 891721312, Level: 18}, n: 890975671, x: 21}, testG{e: lookup.Epoch{Time: 920397342, Level: 11}, n: 920396960, x: 10}, testG{e: lookup.Epoch{Time: 953406530, Level: 3}, n: 953406530, x: 2}, testG{e: lookup.Epoch{Time: 920024527, Level: 23}, n: 920024527, x: 22}, testG{e: lookup.Epoch{Time: 927050922, Level: 7}, n: 927049632, x: 11}, testG{e: lookup.Epoch{Time: 894599900, Level: 10}, n: 890021707, x: 22}, testG{e: lookup.Epoch{Time: 883010150, Level: 3}, n: 882969902, x: 15}, testG{e: lookup.Epoch{Time: 855561102, Level: 22}, n: 855561102, x: 21}, testG{e: lookup.Epoch{Time: 828245477, Level: 19}, n: 825245571, x: 22}, testG{e: lookup.Epoch{Time: 851095026, Level: 4}, n: 851083702, x: 13}, testG{e: lookup.Epoch{Time: 879209039, Level: 11}, n: 879209039, x: 10}, testG{e: lookup.Epoch{Time: 859265651, Level: 0}, n: 840582083, x: 24}, testG{e: lookup.Epoch{Time: 827349870, Level: 24}, n: 827349869, x: 23}, testG{e: lookup.Epoch{Time: 819602318, Level: 3}, n: 18446744073490860182, x: 31}, testG{e: lookup.Epoch{Time: 849708538, Level: 7}, n: 849708538, x: 6}, testG{e: lookup.Epoch{Time: 873885094, Level: 11}, n: 873881798, x: 11}, testG{e: lookup.Epoch{Time: 852169070, Level: 1}, n: 852049399, x: 17}, testG{e: lookup.Epoch{Time: 852885343, Level: 8}, n: 852875652, x: 13}, testG{e: lookup.Epoch{Time: 830957057, Level: 8}, n: 830955867, x: 10}, testG{e: lookup.Epoch{Time: 807353611, Level: 4}, n: 807325211, x: 16}, testG{e: lookup.Epoch{Time: 803198793, Level: 8}, n: 696477575, x: 26}, testG{e: lookup.Epoch{Time: 791356887, Level: 10}, n: 791356003, x: 10}, testG{e: lookup.Epoch{Time: 817771215, Level: 12}, n: 817708431, x: 17}, testG{e: lookup.Epoch{Time: 846211146, Level: 14}, n: 846211146, x: 13}, testG{e: lookup.Epoch{Time: 821849822, Level: 9}, n: 821849229, x: 9}, testG{e: lookup.Epoch{Time: 789508756, Level: 9}, n: 789508755, x: 8}, testG{e: lookup.Epoch{Time: 814088521, Level: 12}, n: 814088512, x: 11}, testG{e: lookup.Epoch{Time: 813665673, Level: 6}, n: 813548257, x: 17}, testG{e: lookup.Epoch{Time: 791472209, Level: 6}, n: 720857845, x: 26}, testG{e: lookup.Epoch{Time: 805687744, Level: 2}, n: 805687720, x: 6}, testG{e: lookup.Epoch{Time: 783153927, Level: 12}, n: 783134053, x: 14}, testG{e: lookup.Epoch{Time: 815033655, Level: 11}, n: 815033654, x: 10}, testG{e: lookup.Epoch{Time: 821184581, Level: 6}, n: 821184464, x: 11}, testG{e: lookup.Epoch{Time: 841908114, Level: 2}, n: 841636025, x: 18}, testG{e: lookup.Epoch{Time: 862969167, Level: 20}, n: 862919955, x: 19}, testG{e: lookup.Epoch{Time: 887604565, Level: 21}, n: 887604564, x: 20}, testG{e: lookup.Epoch{Time: 863723789, Level: 10}, n: 858274530, x: 22}, testG{e: lookup.Epoch{Time: 851533290, Level: 10}, n: 851531385, x: 11}, testG{e: lookup.Epoch{Time: 826032484, Level: 14}, n: 826032484, x: 13}, testG{e: lookup.Epoch{Time: 819401505, Level: 7}, n: 818943526, x: 18}, testG{e: lookup.Epoch{Time: 800886832, Level: 12}, n: 800563106, x: 19}, testG{e: lookup.Epoch{Time: 780767476, Level: 10}, n: 694450997, x: 26}, testG{e: lookup.Epoch{Time: 789209418, Level: 15}, n: 789209417, x: 14}, testG{e: lookup.Epoch{Time: 816086666, Level: 9}, n: 816034646, x: 18}, testG{e: lookup.Epoch{Time: 835407077, Level: 21}, n: 835407076, x: 20}, testG{e: lookup.Epoch{Time: 846527322, Level: 20}, n: 846527321, x: 19}, testG{e: lookup.Epoch{Time: 850131130, Level: 19}, n: 18446744073670013406, x: 31}, testG{e: lookup.Epoch{Time: 842248607, Level: 24}, n: 783963834, x: 28}, testG{e: lookup.Epoch{Time: 816181999, Level: 2}, n: 816124867, x: 15}, testG{e: lookup.Epoch{Time: 806627026, Level: 17}, n: 756013427, x: 28}, testG{e: lookup.Epoch{Time: 826223084, Level: 4}, n: 826169865, x: 16}, testG{e: lookup.Epoch{Time: 835380147, Level: 21}, n: 835380147, x: 20}, testG{e: lookup.Epoch{Time: 860137874, Level: 3}, n: 860137782, x: 7}, testG{e: lookup.Epoch{Time: 860623757, Level: 8}, n: 860621582, x: 12}, testG{e: lookup.Epoch{Time: 875464114, Level: 24}, n: 875464114, x: 23}, testG{e: lookup.Epoch{Time: 853804052, Level: 6}, n: 853804051, x: 5}, testG{e: lookup.Epoch{Time: 864150903, Level: 14}, n: 854360673, x: 24}, testG{e: lookup.Epoch{Time: 850104561, Level: 23}, n: 850104561, x: 22}, testG{e: lookup.Epoch{Time: 878020186, Level: 24}, n: 878020186, x: 23}, testG{e: lookup.Epoch{Time: 900150940, Level: 8}, n: 899224760, x: 21}, testG{e: lookup.Epoch{Time: 869566202, Level: 2}, n: 869566199, x: 3}, testG{e: lookup.Epoch{Time: 851878045, Level: 5}, n: 851878045, x: 4}, testG{e: lookup.Epoch{Time: 824469671, Level: 12}, n: 824466504, x: 13}, testG{e: lookup.Epoch{Time: 819830223, Level: 9}, n: 816550241, x: 22}, testG{e: lookup.Epoch{Time: 813720249, Level: 20}, n: 801351581, x: 28}, testG{e: lookup.Epoch{Time: 831200185, Level: 20}, n: 830760165, x: 19}, testG{e: lookup.Epoch{Time: 838915973, Level: 9}, n: 838915972, x: 8}, testG{e: lookup.Epoch{Time: 812902644, Level: 5}, n: 812902644, x: 4}, testG{e: lookup.Epoch{Time: 812755887, Level: 3}, n: 812755887, x: 2}, testG{e: lookup.Epoch{Time: 822497779, Level: 8}, n: 822486000, x: 14}, testG{e: lookup.Epoch{Time: 832407585, Level: 9}, n: 579450238, x: 28}, testG{e: lookup.Epoch{Time: 799645403, Level: 23}, n: 799645403, x: 22}, testG{e: lookup.Epoch{Time: 827279665, Level: 2}, n: 826723872, x: 19}, testG{e: lookup.Epoch{Time: 846062554, Level: 6}, n: 765881119, x: 28}, testG{e: lookup.Epoch{Time: 855122998, Level: 6}, n: 855122978, x: 5}, testG{e: lookup.Epoch{Time: 841905104, Level: 4}, n: 751401236, x: 28}, testG{e: lookup.Epoch{Time: 857737438, Level: 12}, n: 325468127, x: 29}, testG{e: lookup.Epoch{Time: 838103691, Level: 18}, n: 779030823, x: 28}, testG{e: lookup.Epoch{Time: 841581240, Level: 22}, n: 841581239, x: 21}} +// TestGetNextLevel tests the lookup.GetNextLevel function func TestGetNextLevel(t *testing.T) { // First, test well-known cases @@ -492,7 +618,7 @@ func TestGetNextLevel(t *testing.T) { } -// cookGetNextLevelTests is used to generate a deterministic +// CookGetNextLevelTests is used to generate a deterministic // set of cases for TestGetNextLevel and thus "freeze" its current behavior func CookGetNextLevelTests(t *testing.T) { st := "" diff --git a/swarm/storage/feed/lookup/store_test.go b/swarm/storage/feed/lookup/store_test.go new file mode 100644 index 0000000000..ed52093191 --- /dev/null +++ b/swarm/storage/feed/lookup/store_test.go @@ -0,0 +1,154 @@ +package lookup_test + +/* +This file contains components to mock a storage for testing +lookup algorithms and measure the number of reads. +*/ + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/swarm/log" + "github.com/ethereum/go-ethereum/swarm/storage/feed/lookup" +) + +// Data is a struct to keep a value to store/retrieve during testing +type Data struct { + Payload uint64 + Time uint64 +} + +// String implements fmt.Stringer +func (d *Data) String() string { + return fmt.Sprintf("%d-%d", d.Payload, d.Time) +} + +// Datamap is an internal map to hold the mocked storage +type DataMap map[lookup.EpochID]*Data + +// StoreConfig allows to specify the simulated delays for each type of +// read operation +type StoreConfig struct { + CacheReadTime time.Duration // time it takes to read from the cache + FailedReadTime time.Duration // time it takes to acknowledge a read as failed + SuccessfulReadTime time.Duration // time it takes to fetch data +} + +// StoreCounters will track read count metrics +type StoreCounters struct { + reads int + cacheHits int + failed int + successful int + canceled int + maxSimultaneous int +} + +// Store simulates a store and keeps track of performance counters +type Store struct { + StoreConfig + StoreCounters + data DataMap + cache DataMap + lock sync.RWMutex + activeReads int +} + +// NewStore returns a new mock store ready for use +func NewStore(config *StoreConfig) *Store { + store := &Store{ + StoreConfig: *config, + data: make(DataMap), + } + + store.Reset() + return store +} + +// Reset reset performance counters and clears the cache +func (s *Store) Reset() { + s.cache = make(DataMap) + s.StoreCounters = StoreCounters{} +} + +// Put stores a value in the mock store at the given epoch +func (s *Store) Put(epoch lookup.Epoch, value *Data) { + log.Debug("Write: %d-%d, value='%d'\n", epoch.Base(), epoch.Level, value.Payload) + s.data[epoch.ID()] = value +} + +// Update runs the seed algorithm to place the update in the appropriate epoch +func (s *Store) Update(last lookup.Epoch, now uint64, value *Data) lookup.Epoch { + epoch := lookup.GetNextEpoch(last, now) + s.Put(epoch, value) + return epoch +} + +// Get retrieves data at the specified epoch, simulating a delay +func (s *Store) Get(ctx context.Context, epoch lookup.Epoch, now uint64) (value interface{}, err error) { + epochID := epoch.ID() + var operationTime time.Duration + + defer func() { // simulate a delay according to what has actually happened + select { + case <-lookup.TimeAfter(operationTime): + case <-ctx.Done(): + s.lock.Lock() + s.canceled++ + s.lock.Unlock() + value = nil + err = ctx.Err() + } + s.lock.Lock() + s.activeReads-- + s.lock.Unlock() + }() + + s.lock.Lock() + defer s.lock.Unlock() + s.reads++ + s.activeReads++ + if s.activeReads > s.maxSimultaneous { + s.maxSimultaneous = s.activeReads + } + + // 1.- Simulate a cache read + item := s.cache[epochID] + operationTime += s.CacheReadTime + + if item != nil { + s.cacheHits++ + if item.Time <= now { + s.successful++ + return item, nil + } + return nil, nil + } + + // 2.- simulate a full read + + item = s.data[epochID] + if item != nil { + operationTime += s.SuccessfulReadTime + s.successful++ + s.cache[epochID] = item + if item.Time <= now { + return item, nil + } + } else { + operationTime += s.FailedReadTime + s.failed++ + } + return nil, nil +} + +// MakeReadFunc returns a read function suitable for the lookup algorithm, mapped +// to this mock storage +func (s *Store) MakeReadFunc() lookup.ReadFunc { + return func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) { + return s.Get(ctx, epoch, now) + } +} diff --git a/swarm/storage/feed/lookup/timesim_test.go b/swarm/storage/feed/lookup/timesim_test.go new file mode 100644 index 0000000000..2a254188cc --- /dev/null +++ b/swarm/storage/feed/lookup/timesim_test.go @@ -0,0 +1,128 @@ +package lookup_test + +// This file contains simple time simulation tools for testing +// and measuring time-aware algorithms + +import ( + "sync" + "time" +) + +// Timer tracks information about a simulated timer +type Timer struct { + deadline time.Time + signal chan time.Time + id int +} + +// Stopwatch measures simulated execution time and manages simulated timers +type Stopwatch struct { + t time.Time + resolution time.Duration + timers map[int]*Timer + timerCounter int + stopSignal chan struct{} + lock sync.RWMutex +} + +// NewStopwatch returns a simulated clock that ticks on `resolution` intervals +func NewStopwatch(resolution time.Duration) *Stopwatch { + s := &Stopwatch{ + resolution: resolution, + } + s.Reset() + return s +} + +// Reset clears all timers and sents the stopwatch to zero +func (s *Stopwatch) Reset() { + s.t = time.Time{} + s.timers = make(map[int]*Timer) + s.Stop() +} + +// Tick advances simulated time by the stopwatch's resolution and triggers +// all due timers +func (s *Stopwatch) Tick() { + s.t = s.t.Add(s.resolution) + + s.lock.Lock() + defer s.lock.Unlock() + + for id, timer := range s.timers { + if s.t.After(timer.deadline) || s.t.Equal(timer.deadline) { + timer.signal <- s.t + close(timer.signal) + delete(s.timers, id) + } + } +} + +// NewTimer returns a new timer that will trigger after `duration` elapses in the +// simulation +func (s *Stopwatch) NewTimer(duration time.Duration) <-chan time.Time { + s.lock.Lock() + defer s.lock.Unlock() + + s.timerCounter++ + timer := &Timer{ + deadline: s.t.Add(duration), + signal: make(chan time.Time, 1), + id: s.timerCounter, + } + + s.timers[timer.id] = timer + return timer.signal +} + +// TimeAfter returns a simulated timer factory that can replace `time.After` +func (s *Stopwatch) TimeAfter() func(d time.Duration) <-chan time.Time { + return func(d time.Duration) <-chan time.Time { + return s.NewTimer(d) + } +} + +// Elapsed returns the time that has passed in the simulation +func (s *Stopwatch) Elapsed() time.Duration { + return s.t.Sub(time.Time{}) +} + +// Run starts the time simulation +func (s *Stopwatch) Run() { + go func() { + stopSignal := make(chan struct{}) + s.lock.Lock() + if s.stopSignal != nil { + close(s.stopSignal) + } + s.stopSignal = stopSignal + s.lock.Unlock() + for { + select { + case <-time.After(1 * time.Millisecond): + s.Tick() + case <-stopSignal: + return + } + } + }() +} + +// Stop stops the time simulation +func (s *Stopwatch) Stop() { + s.lock.Lock() + defer s.lock.Unlock() + + if s.stopSignal != nil { + close(s.stopSignal) + s.stopSignal = nil + } +} + +func (s *Stopwatch) Measure(measuredFunc func()) time.Duration { + s.Reset() + s.Run() + defer s.Stop() + measuredFunc() + return s.Elapsed() +} diff --git a/swarm/storage/feed/query_test.go b/swarm/storage/feed/query_test.go index 9fa5e29800..1ec45762e4 100644 --- a/swarm/storage/feed/query_test.go +++ b/swarm/storage/feed/query_test.go @@ -30,7 +30,7 @@ func getTestQuery() *Query { } func TestQueryValues(t *testing.T) { - var expected = KV{"hint.level": "25", "hint.time": "1000", "time": "5000", "topic": "0x776f726c64206e657773207265706f72742c20657665727920686f7572000000", "user": "0x876A8936A7Cd0b79Ef0735AD0896c1AFe278781c"} + var expected = KV{"hint.level": "31", "hint.time": "1000", "time": "5000", "topic": "0x776f726c64206e657773207265706f72742c20657665727920686f7572000000", "user": "0x876A8936A7Cd0b79Ef0735AD0896c1AFe278781c"} query := getTestQuery() testValueSerializer(t, query, expected) diff --git a/swarm/storage/feed/request_test.go b/swarm/storage/feed/request_test.go index c30158fddf..b9c1381c65 100644 --- a/swarm/storage/feed/request_test.go +++ b/swarm/storage/feed/request_test.go @@ -223,7 +223,7 @@ func TestUpdateChunkSerializationErrorChecking(t *testing.T) { t.Fatalf("error creating update chunk:%s", err) } - compareByteSliceToExpectedHex(t, "chunk", chunk.Data(), "0x0000000000000000776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce803000000000019416c206269656e206861636572206a616dc3a173206c652066616c7461207072656d696f5a0ffe0bc27f207cd5b00944c8b9cee93e08b89b5ada777f123ac535189333f174a6a4ca2f43a92c4a477a49d774813c36ce8288552c58e6205b0ac35d0507eb00") + compareByteSliceToExpectedHex(t, "chunk", chunk.Data(), "0x0000000000000000776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce80300000000001f416c206269656e206861636572206a616dc3a173206c652066616c7461207072656d696f9896df5937e64e51a7994479ff3fe0ed790d539b9b3e85e93c0014a8a64374f23603c79d16e99b50a757896d3816d7022ac594ad1415679a9b164afb2e5926d801") var recovered Request recovered.fromChunk(chunk) diff --git a/swarm/storage/feed/update_test.go b/swarm/storage/feed/update_test.go index 24c09b3617..e4e0963e9f 100644 --- a/swarm/storage/feed/update_test.go +++ b/swarm/storage/feed/update_test.go @@ -28,7 +28,7 @@ func getTestFeedUpdate() *Update { } func TestUpdateSerializer(t *testing.T) { - testBinarySerializerRecovery(t, getTestFeedUpdate(), "0x0000000000000000776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce803000000000019456c20717565206c6565206d7563686f207920616e6461206d7563686f2c207665206d7563686f20792073616265206d7563686f") + testBinarySerializerRecovery(t, getTestFeedUpdate(), "0x0000000000000000776f726c64206e657773207265706f72742c20657665727920686f7572000000876a8936a7cd0b79ef0735ad0896c1afe278781ce80300000000001f456c20717565206c6565206d7563686f207920616e6461206d7563686f2c207665206d7563686f20792073616265206d7563686f") } func TestUpdateLengthCheck(t *testing.T) { From 9eba3a9fff2f47f5e094c36a7c905380b0ac8b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 16 May 2019 14:30:11 +0300 Subject: [PATCH 27/34] cmd/geth, core/rawdb: seamless freezer consistency, friendly removedb --- cmd/geth/chaincmd.go | 234 +++++++------------------------ cmd/geth/main.go | 1 - cmd/geth/os_unix.go | 51 ------- cmd/geth/os_windows.go | 43 ------ console/prompter.go | 2 +- core/blockchain_test.go | 5 +- core/rawdb/accessors_metadata.go | 14 -- core/rawdb/database.go | 80 +++++++++-- core/rawdb/freezer_table.go | 2 +- core/rawdb/schema.go | 3 - 10 files changed, 122 insertions(+), 313 deletions(-) delete mode 100644 cmd/geth/os_unix.go delete mode 100644 cmd/geth/os_windows.go diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 70164f82b2..c91545c7fc 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -18,10 +18,7 @@ package main import ( "encoding/json" - "errors" "fmt" - "io" - "io/ioutil" "os" "path/filepath" "runtime" @@ -171,21 +168,6 @@ Remove blockchain and state databases`, The arguments are interpreted as block numbers or hashes. Use "ethereum dump 0" to dump the genesis block.`, } - migrateAncientCommand = cli.Command{ - Action: utils.MigrateFlags(migrateAncient), - Name: "migrate-ancient", - Usage: "migrate ancient database forcibly", - ArgsUsage: " ", - Flags: []cli.Flag{ - utils.DataDirFlag, - utils.AncientFlag, - utils.CacheFlag, - utils.TestnetFlag, - utils.RinkebyFlag, - utils.GoerliFlag, - }, - Category: "BLOCKCHAIN COMMANDS", - } inspectCommand = cli.Command{ Action: utils.MigrateFlags(inspect), Name: "inspect", @@ -460,51 +442,63 @@ func copyDb(ctx *cli.Context) error { func removeDB(ctx *cli.Context) error { stack, config := makeConfigNode(ctx) - for i, name := range []string{"chaindata", "lightchaindata"} { - // Ensure the database exists in the first place - logger := log.New("database", name) - - var ( - dbdirs []string - freezer string - ) - dbdir := stack.ResolvePath(name) - if !common.FileExist(dbdir) { - logger.Info("Database doesn't exist, skipping", "path", dbdir) - continue - } - dbdirs = append(dbdirs, dbdir) - if i == 0 { - freezer = config.Eth.DatabaseFreezer - switch { - case freezer == "": - freezer = filepath.Join(dbdir, "ancient") - case !filepath.IsAbs(freezer): - freezer = config.Node.ResolvePath(freezer) - } - if common.FileExist(freezer) { - dbdirs = append(dbdirs, freezer) - } - } - for i := len(dbdirs) - 1; i >= 0; i-- { - // Confirm removal and execute - fmt.Println(dbdirs[i]) - confirm, err := console.Stdin.PromptConfirm("Remove this database?") - switch { - case err != nil: - utils.Fatalf("%v", err) - case !confirm: - logger.Warn("Database deletion aborted") - default: - start := time.Now() - os.RemoveAll(dbdirs[i]) - logger.Info("Database successfully deleted", "elapsed", common.PrettyDuration(time.Since(start))) - } - } + // Remove the full node state database + path := stack.ResolvePath("chaindata") + if common.FileExist(path) { + confirmAndRemoveDB(path, "full node state database") + } else { + log.Info("Full node state database missing", "path", path) + } + // Remove the full node ancient database + path = config.Eth.DatabaseFreezer + switch { + case path == "": + path = filepath.Join(stack.ResolvePath("chaindata"), "ancient") + case !filepath.IsAbs(path): + path = config.Node.ResolvePath(path) + } + if common.FileExist(path) { + confirmAndRemoveDB(path, "full node ancient database") + } else { + log.Info("Full node ancient database missing", "path", path) + } + // Remove the light node database + path = stack.ResolvePath("lightchaindata") + if common.FileExist(path) { + confirmAndRemoveDB(path, "light node database") + } else { + log.Info("Light node database missing", "path", path) } return nil } +// confirmAndRemoveDB prompts the user for a last confirmation and removes the +// folder if accepted. +func confirmAndRemoveDB(database string, kind string) { + confirm, err := console.Stdin.PromptConfirm(fmt.Sprintf("Remove %s (%s)?", kind, database)) + switch { + case err != nil: + utils.Fatalf("%v", err) + case !confirm: + log.Info("Database deletion skipped", "path", database) + default: + start := time.Now() + filepath.Walk(database, func(path string, info os.FileInfo, err error) error { + // If we're at the top level folder, recurse into + if path == database { + return nil + } + // Delete all the files, but not subfolders + if !info.IsDir() { + os.Remove(path) + return nil + } + return filepath.SkipDir + }) + log.Info("Database successfully deleted", "path", database, "elapsed", common.PrettyDuration(time.Since(start))) + } +} + func dump(ctx *cli.Context) error { stack := makeFullNode(ctx) defer stack.Close() @@ -533,47 +527,6 @@ func dump(ctx *cli.Context) error { return nil } -func migrateAncient(ctx *cli.Context) error { - node, config := makeConfigNode(ctx) - defer node.Close() - - dbdir := config.Node.ResolvePath("chaindata") - kvdb, err := rawdb.NewLevelDBDatabase(dbdir, 128, 1024, "") - if err != nil { - return err - } - defer kvdb.Close() - - freezer := config.Eth.DatabaseFreezer - switch { - case freezer == "": - freezer = filepath.Join(dbdir, "ancient") - case !filepath.IsAbs(freezer): - freezer = config.Node.ResolvePath(freezer) - } - stored := rawdb.ReadAncientPath(kvdb) - if stored != freezer && stored != "" { - confirm, err := console.Stdin.PromptConfirm(fmt.Sprintf("Are you sure to migrate ancient database from %s to %s?", stored, freezer)) - switch { - case err != nil: - utils.Fatalf("%v", err) - case !confirm: - log.Warn("Ancient database migration aborted") - default: - if err := rename(stored, freezer); err != nil { - // Renaming a file can fail if the source and destination - // are on different file systems. - if err := moveAncient(stored, freezer); err != nil { - utils.Fatalf("Migrate ancient database failed, %v", err) - } - } - rawdb.WriteAncientPath(kvdb, freezer) - log.Info("Ancient database successfully migrated") - } - } - return nil -} - func inspect(ctx *cli.Context) error { node, _ := makeConfigNode(ctx) defer node.Close() @@ -589,84 +542,3 @@ func hashish(x string) bool { _, err := strconv.Atoi(x) return err != nil } - -// copyFileSynced copies data from source file to destination -// and synces the dest file forcibly. -func copyFileSynced(src string, dest string, info os.FileInfo) error { - srcf, err := os.Open(src) - if err != nil { - return err - } - defer srcf.Close() - - destf, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode().Perm()) - if err != nil { - return err - } - // The maximum size of ancient file is 2GB, 4MB buffer is suitable here. - buff := make([]byte, 4*1024*1024) - for { - rn, err := srcf.Read(buff) - if err != nil && err != io.EOF { - return err - } - if rn == 0 { - break - } - if wn, err := destf.Write(buff[:rn]); err != nil || wn != rn { - return err - } - } - if err1 := destf.Sync(); err == nil { - err = err1 - } - if err1 := destf.Close(); err == nil { - err = err1 - } - return err -} - -// copyDirSynced recursively copies files under the specified dir -// to dest and synces the dest dir forcibly. -func copyDirSynced(src string, dest string, info os.FileInfo) error { - if err := os.MkdirAll(dest, os.ModePerm); err != nil { - return err - } - defer os.Chmod(dest, info.Mode()) - - objects, err := ioutil.ReadDir(src) - if err != nil { - return err - } - for _, obj := range objects { - // All files in ancient database should be flatten files. - if !obj.Mode().IsRegular() { - continue - } - subsrc, subdest := filepath.Join(src, obj.Name()), filepath.Join(dest, obj.Name()) - if err := copyFileSynced(subsrc, subdest, obj); err != nil { - return err - } - } - return syncDir(dest) -} - -// moveAncient migrates ancient database from source to destination. -func moveAncient(src string, dest string) error { - srcinfo, err := os.Stat(src) - if err != nil { - return err - } - if !srcinfo.IsDir() { - return errors.New("ancient directory expected") - } - if destinfo, err := os.Lstat(dest); !os.IsNotExist(err) { - if destinfo.Mode()&os.ModeSymlink != 0 { - return errors.New("symbolic link datadir is not supported") - } - } - if err := copyDirSynced(src, dest, srcinfo); err != nil { - return err - } - return os.RemoveAll(src) -} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index afa39bf933..1b23cbd9f2 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -204,7 +204,6 @@ func init() { copydbCommand, removedbCommand, dumpCommand, - migrateAncientCommand, inspectCommand, // See accountcmd.go: accountCommand, diff --git a/cmd/geth/os_unix.go b/cmd/geth/os_unix.go deleted file mode 100644 index 6722ec9cb6..0000000000 --- a/cmd/geth/os_unix.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2012, Suryandaru Triandana -// All rights reserved. -// -// Use of this source code is governed by a BSD-style license. -// -// +build darwin dragonfly freebsd linux netbsd openbsd solaris - -package main - -import ( - "os" - "syscall" -) - -func rename(oldpath, newpath string) error { - return os.Rename(oldpath, newpath) -} - -func isErrInvalid(err error) bool { - if err == os.ErrInvalid { - return true - } - // Go < 1.8 - if syserr, ok := err.(*os.SyscallError); ok && syserr.Err == syscall.EINVAL { - return true - } - // Go >= 1.8 returns *os.PathError instead - if patherr, ok := err.(*os.PathError); ok && patherr.Err == syscall.EINVAL { - return true - } - return false -} - -func syncDir(name string) error { - // As per fsync manpage, Linux seems to expect fsync on directory, however - // some system don't support this, so we will ignore syscall.EINVAL. - // - // From fsync(2): - // Calling fsync() does not necessarily ensure that the entry in the - // directory containing the file has also reached disk. For that an - // explicit fsync() on a file descriptor for the directory is also needed. - f, err := os.Open(name) - if err != nil { - return err - } - defer f.Close() - if err := f.Sync(); err != nil && !isErrInvalid(err) { - return err - } - return nil -} diff --git a/cmd/geth/os_windows.go b/cmd/geth/os_windows.go deleted file mode 100644 index f2406ec9bc..0000000000 --- a/cmd/geth/os_windows.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2013, Suryandaru Triandana -// All rights reserved. -// -// Use of this source code is governed by a BSD-style license. - -package main - -import ( - "syscall" - "unsafe" -) - -var ( - modkernel32 = syscall.NewLazyDLL("kernel32.dll") - procMoveFileExW = modkernel32.NewProc("MoveFileExW") -) - -const _MOVEFILE_REPLACE_EXISTING = 1 - -func moveFileEx(from *uint16, to *uint16, flags uint32) error { - r1, _, e1 := syscall.Syscall(procMoveFileExW.Addr(), 3, uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(to)), uintptr(flags)) - if r1 == 0 { - if e1 != 0 { - return error(e1) - } - return syscall.EINVAL - } - return nil -} - -func rename(oldpath, newpath string) error { - from, err := syscall.UTF16PtrFromString(oldpath) - if err != nil { - return err - } - to, err := syscall.UTF16PtrFromString(newpath) - if err != nil { - return err - } - return moveFileEx(from, to, _MOVEFILE_REPLACE_EXISTING) -} - -func syncDir(name string) error { return nil } diff --git a/console/prompter.go b/console/prompter.go index 29a53aeadd..65675061a7 100644 --- a/console/prompter.go +++ b/console/prompter.go @@ -142,7 +142,7 @@ func (p *terminalPrompter) PromptPassword(prompt string) (passwd string, err err // PromptConfirm displays the given prompt to the user and requests a boolean // choice to be made, returning that choice. func (p *terminalPrompter) PromptConfirm(prompt string) (bool, error) { - input, err := p.Prompt(prompt + " [y/N] ") + input, err := p.Prompt(prompt + " [y/n] ") if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" { return true, nil } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 09caf7e602..8dfcda6d45 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -1717,10 +1717,7 @@ func TestIncompleteAncientReceiptChainInsertion(t *testing.T) { } // Abort ancient receipt chain insertion deliberately ancient.terminateInsert = func(hash common.Hash, number uint64) bool { - if number == blocks[len(blocks)/2].NumberU64() { - return true - } - return false + return number == blocks[len(blocks)/2].NumberU64() } previousFastBlock := ancient.CurrentFastBlock() if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err == nil { diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go index e6235f0104..f8d09fbddf 100644 --- a/core/rawdb/accessors_metadata.go +++ b/core/rawdb/accessors_metadata.go @@ -80,20 +80,6 @@ func WriteChainConfig(db ethdb.KeyValueWriter, hash common.Hash, cfg *params.Cha } } -// ReadAncientPath retrieves ancient database path which is recorded during the -// first node setup or forcibly changed by user. -func ReadAncientPath(db ethdb.KeyValueReader) string { - data, _ := db.Get(ancientKey) - return string(data) -} - -// WriteAncientPath writes ancient database path into the key-value database. -func WriteAncientPath(db ethdb.KeyValueWriter, path string) { - if err := db.Put(ancientKey, []byte(path)); err != nil { - log.Crit("Failed to store ancient path", "err", err) - } -} - // ReadPreimage retrieves a single preimage of the provided hash. func ReadPreimage(db ethdb.KeyValueReader, hash common.Hash) []byte { data, _ := db.Get(preimageKey(hash)) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 37379147cd..353b7dce62 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -18,6 +18,7 @@ package rawdb import ( "bytes" + "errors" "fmt" "os" "time" @@ -104,10 +105,74 @@ func NewDatabase(db ethdb.KeyValueStore) ethdb.Database { // value data store with a freezer moving immutable chain segments into cold // storage. func NewDatabaseWithFreezer(db ethdb.KeyValueStore, freezer string, namespace string) (ethdb.Database, error) { + // Create the idle freezer instance frdb, err := newFreezer(freezer, namespace) if err != nil { return nil, err } + // Since the freezer can be stored separately from the user's key-value database, + // there's a fairly high probability that the user requests invalid combinations + // of the freezer and database. Ensure that we don't shoot ourselves in the foot + // by serving up conflicting data, leading to both datastores getting corrupted. + // + // - If both the freezer and key-value store is empty (no genesis), we just + // initialized a new empty freezer, so everything's fine. + // - If the key-value store is empty, but the freezer is not, we need to make + // sure the user's genesis matches the freezer. That will be checked in the + // blockchain, since we don't have the genesis block here (nor should we at + // this point care, the key-value/freezer combo is valid). + // - If neither the key-value store nor the freezer is empty, cross validate + // the genesis hashes to make sure they are compatible. If they are, also + // ensure that there's no gap between the freezer and sunsequently leveldb. + // - If the key-value store is not empty, but the freezer is we might just be + // upgrading to the freezer release, or we might have had a small chain and + // not frozen anything yet. Ensure that no blocks are missing yet from the + // key-value store, since that would mean we already had an old freezer. + + // If the genesis hash is empty, we have a new key-value store, so nothing to + // validate in this method. If, however, the genesis hash is not nil, compare + // it to the freezer content. + if kvgenesis, _ := db.Get(headerHashKey(0)); len(kvgenesis) > 0 { + if frozen, _ := frdb.Ancients(); frozen > 0 { + // If the freezer already contains something, ensure that the genesis blocks + // match, otherwise we might mix up freezers across chains and destroy both + // the freezer and the key-value store. + if frgenesis, _ := frdb.Ancient(freezerHashTable, 0); !bytes.Equal(kvgenesis, frgenesis) { + return nil, fmt.Errorf("genesis mismatch: %#x (leveldb) != %#x (ancients)", kvgenesis, frgenesis) + } + // Key-value store and freezer belong to the same network. Ensure that they + // are contiguous, otherwise we might end up with a non-functional freezer. + if kvhash, _ := db.Get(headerHashKey(frozen)); len(kvhash) == 0 { + // Subsequent header after the freezer limit is missing from the database. + // Reject startup is the database has a more recent head. + if *ReadHeaderNumber(db, ReadHeadHeaderHash(db)) > frozen-1 { + return nil, fmt.Errorf("gap (#%d) in the chain between ancients and leveldb", frozen) + } + // Database contains only older data than the freezer, this happens if the + // state was wiped and reinited from an existing freezer. + } else { + // Key-value store continues where the freezer left off, all is fine. We might + // have duplicate blocks (crash after freezer write but before kay-value store + // deletion, but that's fine). + } + } else { + // If the freezer is empty, ensure nothing was moved yet from the key-value + // store, otherwise we'll end up missing data. We check block #1 to decide + // if we froze anything previously or not, but do take care of databases with + // only the genesis block. + if ReadHeadHeaderHash(db) != common.BytesToHash(kvgenesis) { + // Key-value store contains more data than the genesis block, make sure we + // didn't freeze anything yet. + if kvblob, _ := db.Get(headerHashKey(1)); len(kvblob) == 0 { + return nil, errors.New("ancient chain segments already extracted, please set --datadir.ancient to the correct path") + } + // Block #1 is still in the database, we're allowed to init a new feezer + } else { + // The head header is still the genesis, we're allowed to init a new feezer + } + } + } + // Freezer is consistent with the key-value database, permit combining the two go frdb.freeze(db) return &freezerdb{ @@ -151,19 +216,6 @@ func NewLevelDBDatabaseWithFreezer(file string, cache int, handles int, freezer kvdb.Close() return nil, err } - // Make sure we always use the same ancient store. - // - // | stored == nil | stored != nil - // ----------------+------------------+---------------------- - // freezer == nil | non-freezer mode | ancient store missing - // freezer != nil | initialize | ensure consistency - stored := ReadAncientPath(kvdb) - if stored == "" && freezer != "" { - WriteAncientPath(kvdb, freezer) - } else if stored != freezer { - log.Warn("Ancient path mismatch", "stored", stored, "given", freezer) - log.Crit("Please use a consistent ancient path or migrate it via the command line tool `geth migrate-ancient`") - } return frdb, nil } @@ -243,7 +295,7 @@ func InspectDatabase(db ethdb.Database) error { trieSize += size default: var accounted bool - for _, meta := range [][]byte{databaseVerisionKey, headHeaderKey, headBlockKey, headFastBlockKey, fastTrieProgressKey, ancientKey} { + for _, meta := range [][]byte{databaseVerisionKey, headHeaderKey, headBlockKey, headFastBlockKey, fastTrieProgressKey} { if bytes.Equal(key, meta) { metadata += size accounted = true diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index ebccf78169..673a181e49 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -259,7 +259,7 @@ func (t *freezerTable) preopen() (err error) { // The repair might have already opened (some) files t.releaseFilesAfter(0, false) // Open all except head in RDONLY - for i := uint32(t.tailId); i < t.headId; i++ { + for i := t.tailId; i < t.headId; i++ { if _, err = t.openFile(i, os.O_RDONLY); err != nil { return err } diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 0d54a3c8b1..a44a2c99f9 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -41,9 +41,6 @@ var ( // fastTrieProgressKey tracks the number of trie entries imported during fast sync. fastTrieProgressKey = []byte("TrieSync") - // ancientKey tracks the absolute path of ancient database. - ancientKey = []byte("AncientPath") - // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes). headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td From 60386b3545659d99c7488e456c780606db101936 Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Thu, 16 May 2019 17:29:12 +0200 Subject: [PATCH 28/34] swarm/network: bump network id for 0.4 release (#19580) * swarm/network: bump network id for 0.4 release * swarm/network: bump bzz protocol version * swarm/docs: migration document v0.3 to v0.4 * swarm/storage/feed: gofmt lookup_test.go --- swarm/docs/migration-v0.3-to-v0.4.md | 31 ++++++++++++++++++++++++ swarm/network/protocol.go | 4 +-- swarm/network/protocol_test.go | 2 +- swarm/network/stream/syncer.go | 13 +++++----- swarm/storage/feed/lookup/lookup_test.go | 2 +- 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 swarm/docs/migration-v0.3-to-v0.4.md diff --git a/swarm/docs/migration-v0.3-to-v0.4.md b/swarm/docs/migration-v0.3-to-v0.4.md new file mode 100644 index 0000000000..cebc286c19 --- /dev/null +++ b/swarm/docs/migration-v0.3-to-v0.4.md @@ -0,0 +1,31 @@ +Swarm DB migration notes +========================= +Swarm `v0.4` introduces major changes to the existing codebase. Among other things, the storage layer has been rewritten to be more modular and flexible +in a manner that will accomodate for our future needs. Since Swarm at this point does not provide any storage guarantees, we have made the decision to not impose any migrations on our public cluster nor on our users. What this essentially means is that local storage will be purged on `v0.4`. We have nevertheless, provided a procedure below for those of you running private clusters and would like to migrate the data to the new local storage format. + +You are highly encouraged to report to us any bugs or problems caused by running the migration steps below. + +**Note**: we highly recommend you run the commands below with `--verbosity 5` flag and open an issue with the relevant terminal output in case something goes wrong. + +**Important**: since you would be creating an export of your local store, the potential disk usage might peak at `x2-x3` times the normal Swarm data folder size. Please make sure you have enough disk space, backup mediums or other form of local/network attached storage _before_ executing the following steps! + +**Important**: when trying to run Swarm with an old local store format, the Swarm binary will refuse to start showing an error message. + +You will need the following information for the migration procedure: +1. Your `datadir` path. This is indicated with the `--datadir` flag when running Swarm. If you do not specify this flag, the `datadir` will reside by default on `$HOME/.ethereum`. +2. Your chunk directory location. This would normally be located in your `datadir/swarm/bzz-/chunks`. We will refer to this as `chunkDir` below. +3. Your `bzzAddr`. This is _not_ your `--bzzaccount`! You can find your `bzzAddr` when starting Swarm by looking for the following line: +``` +INFO [03-21|17:25:04.791] Swarm network started bzzaddr=ca1e9f3938cc1425c6061b96ad9eb93e134dfe8734ad490164ef20af9d1cf59c +``` + +The migration process is done in the following manner: +1. Try to run the updated Swarm binary, it should complain about the local store format and exit. If it does - execute the following steps: +2. `$ swarm --verbosity 5 db export /.tar ` +3. Move or Remove your existing `chunkDir` +4. Run the new Swarm binary as your would start your Swarm node normally. The binary should now load normally and not complain. This step creates a new empty chunk store. Please shut down the node after it starts correctly. +5. `$ swarm --verbosity 5 db import --legacy /.tar ` +6. Wait patientally for the `Imported X chunks successfully` message. +7. Start your Swarm node as you normally would +8. Have a beer + diff --git a/swarm/network/protocol.go b/swarm/network/protocol.go index ad3f8df8f9..dd8a86c0b3 100644 --- a/swarm/network/protocol.go +++ b/swarm/network/protocol.go @@ -33,7 +33,7 @@ import ( ) const ( - DefaultNetworkID = 3 + DefaultNetworkID = 4 // timeout for waiting bzzHandshakeTimeout = 3000 * time.Millisecond ) @@ -43,7 +43,7 @@ var DefaultTestNetworkID = rand.Uint64() // BzzSpec is the spec of the generic swarm handshake var BzzSpec = &protocols.Spec{ Name: "bzz", - Version: 8, + Version: 9, MaxMsgSize: 10 * 1024 * 1024, Messages: []interface{}{ HandshakeMsg{}, diff --git a/swarm/network/protocol_test.go b/swarm/network/protocol_test.go index 737ad0784f..616abde9ed 100644 --- a/swarm/network/protocol_test.go +++ b/swarm/network/protocol_test.go @@ -36,7 +36,7 @@ import ( ) const ( - TestProtocolVersion = 8 + TestProtocolVersion = 9 ) var TestProtocolNetworkID = DefaultTestNetworkID diff --git a/swarm/network/stream/syncer.go b/swarm/network/stream/syncer.go index 9bde39550b..7a61950eda 100644 --- a/swarm/network/stream/syncer.go +++ b/swarm/network/stream/syncer.go @@ -18,6 +18,7 @@ package stream import ( "context" + "fmt" "strconv" "time" @@ -58,7 +59,7 @@ func RegisterSwarmSyncerServer(streamer *Registry, netStore *storage.NetStore) { if err != nil { return nil, err } - return NewSwarmSyncerServer(po, netStore, p.ID().String()+"|"+string(po)) + return NewSwarmSyncerServer(po, netStore, fmt.Sprintf("%s|%d", p.ID(), po)) }) // streamer.RegisterServerFunc(stream, func(p *Peer) (Server, error) { // return NewOutgoingProvableSwarmSyncer(po, db) @@ -146,16 +147,16 @@ func (s *SwarmSyncerServer) SetNextBatch(from, to uint64) ([]byte, uint64, uint6 if batchSize >= BatchSize { iterate = false metrics.GetOrRegisterCounter("syncer.set-next-batch.full-batch", nil).Inc(1) - log.Debug("syncer pull subscription - batch size reached", "correlateId", s.correlateId, "batchSize", batchSize, "batchStartID", batchStartID, "batchEndID", batchEndID) + log.Trace("syncer pull subscription - batch size reached", "correlateId", s.correlateId, "batchSize", batchSize, "batchStartID", batchStartID, "batchEndID", batchEndID) } if timer == nil { timer = time.NewTimer(batchTimeout) } else { - log.Debug("syncer pull subscription - stopping timer", "correlateId", s.correlateId) + log.Trace("syncer pull subscription - stopping timer", "correlateId", s.correlateId) if !timer.Stop() { <-timer.C } - log.Debug("syncer pull subscription - channel drained, resetting timer", "correlateId", s.correlateId) + log.Trace("syncer pull subscription - channel drained, resetting timer", "correlateId", s.correlateId) timer.Reset(batchTimeout) } timerC = timer.C @@ -164,10 +165,10 @@ func (s *SwarmSyncerServer) SetNextBatch(from, to uint64) ([]byte, uint64, uint6 // received after some time iterate = false metrics.GetOrRegisterCounter("syncer.set-next-batch.timer-expire", nil).Inc(1) - log.Debug("syncer pull subscription timer expired", "correlateId", s.correlateId, "batchSize", batchSize, "batchStartID", batchStartID, "batchEndID", batchEndID) + log.Trace("syncer pull subscription timer expired", "correlateId", s.correlateId, "batchSize", batchSize, "batchStartID", batchStartID, "batchEndID", batchEndID) case <-s.quit: iterate = false - log.Debug("syncer pull subscription - quit received", "correlateId", s.correlateId, "batchSize", batchSize, "batchStartID", batchStartID, "batchEndID", batchEndID) + log.Trace("syncer pull subscription - quit received", "correlateId", s.correlateId, "batchSize", batchSize, "batchStartID", batchStartID, "batchEndID", batchEndID) } } if batchStartID == nil { diff --git a/swarm/storage/feed/lookup/lookup_test.go b/swarm/storage/feed/lookup/lookup_test.go index b5d1a3b0ba..b0d132de64 100644 --- a/swarm/storage/feed/lookup/lookup_test.go +++ b/swarm/storage/feed/lookup/lookup_test.go @@ -572,7 +572,7 @@ type testG struct { } // test cases -var testGetNextLevelCases = []testG{testG{e: lookup.Epoch{Time: 989875233, Level: 12}, n: 989807323, x: 24}, testG{e: lookup.Epoch{Time: 995807650, Level: 18}, n: 995807649, x: 17}, testG{e: lookup.Epoch{Time: 969167082, Level: 0}, n: 969111431, x: 18}, testG{e: lookup.Epoch{Time: 993087628, Level: 14}, n: 993087627, x: 13}, testG{e: lookup.Epoch{Time: 963364631, Level: 20}, n: 962941578, x: 19}, testG{e: lookup.Epoch{Time: 963497510, Level: 16}, n: 963497509, x: 15}, testG{e: lookup.Epoch{Time: 955421349, Level: 22}, n: 929292183, x: 27}, testG{e: lookup.Epoch{Time: 968220379, Level: 15}, n: 968220378, x: 14}, testG{e: lookup.Epoch{Time: 939129014, Level: 6}, n: 939126953, x: 11}, testG{e: lookup.Epoch{Time: 907847903, Level: 6}, n: 907846146, x: 11}, testG{e: lookup.Epoch{Time: 910835564, Level: 15}, n: 703619757, x: 28}, testG{e: lookup.Epoch{Time: 913578333, Level: 22}, n: 913578332, x: 21}, testG{e: lookup.Epoch{Time: 895818460, Level: 3}, n: 895818132, x: 9}, testG{e: lookup.Epoch{Time: 903843025, Level: 24}, n: 903843025, x: 23}, testG{e: lookup.Epoch{Time: 877889433, Level: 13}, n: 149120378, x: 29}, testG{e: lookup.Epoch{Time: 901450396, Level: 10}, n: 858997793, x: 26}, testG{e: lookup.Epoch{Time: 925179910, Level: 3}, n: 925177237, x: 13}, testG{e: lookup.Epoch{Time: 913485477, Level: 21}, n: 907146511, x: 22}, testG{e: lookup.Epoch{Time: 924462991, Level: 18}, n: 924462990, x: 17}, testG{e: lookup.Epoch{Time: 941175128, Level: 13}, n: 941168924, x: 13}, testG{e: lookup.Epoch{Time: 920126583, Level: 3}, n: 538054817, x: 28}, testG{e: lookup.Epoch{Time: 891721312, Level: 18}, n: 890975671, x: 21}, testG{e: lookup.Epoch{Time: 920397342, Level: 11}, n: 920396960, x: 10}, testG{e: lookup.Epoch{Time: 953406530, Level: 3}, n: 953406530, x: 2}, testG{e: lookup.Epoch{Time: 920024527, Level: 23}, n: 920024527, x: 22}, testG{e: lookup.Epoch{Time: 927050922, Level: 7}, n: 927049632, x: 11}, testG{e: lookup.Epoch{Time: 894599900, Level: 10}, n: 890021707, x: 22}, testG{e: lookup.Epoch{Time: 883010150, Level: 3}, n: 882969902, x: 15}, testG{e: lookup.Epoch{Time: 855561102, Level: 22}, n: 855561102, x: 21}, testG{e: lookup.Epoch{Time: 828245477, Level: 19}, n: 825245571, x: 22}, testG{e: lookup.Epoch{Time: 851095026, Level: 4}, n: 851083702, x: 13}, testG{e: lookup.Epoch{Time: 879209039, Level: 11}, n: 879209039, x: 10}, testG{e: lookup.Epoch{Time: 859265651, Level: 0}, n: 840582083, x: 24}, testG{e: lookup.Epoch{Time: 827349870, Level: 24}, n: 827349869, x: 23}, testG{e: lookup.Epoch{Time: 819602318, Level: 3}, n: 18446744073490860182, x: 31}, testG{e: lookup.Epoch{Time: 849708538, Level: 7}, n: 849708538, x: 6}, testG{e: lookup.Epoch{Time: 873885094, Level: 11}, n: 873881798, x: 11}, testG{e: lookup.Epoch{Time: 852169070, Level: 1}, n: 852049399, x: 17}, testG{e: lookup.Epoch{Time: 852885343, Level: 8}, n: 852875652, x: 13}, testG{e: lookup.Epoch{Time: 830957057, Level: 8}, n: 830955867, x: 10}, testG{e: lookup.Epoch{Time: 807353611, Level: 4}, n: 807325211, x: 16}, testG{e: lookup.Epoch{Time: 803198793, Level: 8}, n: 696477575, x: 26}, testG{e: lookup.Epoch{Time: 791356887, Level: 10}, n: 791356003, x: 10}, testG{e: lookup.Epoch{Time: 817771215, Level: 12}, n: 817708431, x: 17}, testG{e: lookup.Epoch{Time: 846211146, Level: 14}, n: 846211146, x: 13}, testG{e: lookup.Epoch{Time: 821849822, Level: 9}, n: 821849229, x: 9}, testG{e: lookup.Epoch{Time: 789508756, Level: 9}, n: 789508755, x: 8}, testG{e: lookup.Epoch{Time: 814088521, Level: 12}, n: 814088512, x: 11}, testG{e: lookup.Epoch{Time: 813665673, Level: 6}, n: 813548257, x: 17}, testG{e: lookup.Epoch{Time: 791472209, Level: 6}, n: 720857845, x: 26}, testG{e: lookup.Epoch{Time: 805687744, Level: 2}, n: 805687720, x: 6}, testG{e: lookup.Epoch{Time: 783153927, Level: 12}, n: 783134053, x: 14}, testG{e: lookup.Epoch{Time: 815033655, Level: 11}, n: 815033654, x: 10}, testG{e: lookup.Epoch{Time: 821184581, Level: 6}, n: 821184464, x: 11}, testG{e: lookup.Epoch{Time: 841908114, Level: 2}, n: 841636025, x: 18}, testG{e: lookup.Epoch{Time: 862969167, Level: 20}, n: 862919955, x: 19}, testG{e: lookup.Epoch{Time: 887604565, Level: 21}, n: 887604564, x: 20}, testG{e: lookup.Epoch{Time: 863723789, Level: 10}, n: 858274530, x: 22}, testG{e: lookup.Epoch{Time: 851533290, Level: 10}, n: 851531385, x: 11}, testG{e: lookup.Epoch{Time: 826032484, Level: 14}, n: 826032484, x: 13}, testG{e: lookup.Epoch{Time: 819401505, Level: 7}, n: 818943526, x: 18}, testG{e: lookup.Epoch{Time: 800886832, Level: 12}, n: 800563106, x: 19}, testG{e: lookup.Epoch{Time: 780767476, Level: 10}, n: 694450997, x: 26}, testG{e: lookup.Epoch{Time: 789209418, Level: 15}, n: 789209417, x: 14}, testG{e: lookup.Epoch{Time: 816086666, Level: 9}, n: 816034646, x: 18}, testG{e: lookup.Epoch{Time: 835407077, Level: 21}, n: 835407076, x: 20}, testG{e: lookup.Epoch{Time: 846527322, Level: 20}, n: 846527321, x: 19}, testG{e: lookup.Epoch{Time: 850131130, Level: 19}, n: 18446744073670013406, x: 31}, testG{e: lookup.Epoch{Time: 842248607, Level: 24}, n: 783963834, x: 28}, testG{e: lookup.Epoch{Time: 816181999, Level: 2}, n: 816124867, x: 15}, testG{e: lookup.Epoch{Time: 806627026, Level: 17}, n: 756013427, x: 28}, testG{e: lookup.Epoch{Time: 826223084, Level: 4}, n: 826169865, x: 16}, testG{e: lookup.Epoch{Time: 835380147, Level: 21}, n: 835380147, x: 20}, testG{e: lookup.Epoch{Time: 860137874, Level: 3}, n: 860137782, x: 7}, testG{e: lookup.Epoch{Time: 860623757, Level: 8}, n: 860621582, x: 12}, testG{e: lookup.Epoch{Time: 875464114, Level: 24}, n: 875464114, x: 23}, testG{e: lookup.Epoch{Time: 853804052, Level: 6}, n: 853804051, x: 5}, testG{e: lookup.Epoch{Time: 864150903, Level: 14}, n: 854360673, x: 24}, testG{e: lookup.Epoch{Time: 850104561, Level: 23}, n: 850104561, x: 22}, testG{e: lookup.Epoch{Time: 878020186, Level: 24}, n: 878020186, x: 23}, testG{e: lookup.Epoch{Time: 900150940, Level: 8}, n: 899224760, x: 21}, testG{e: lookup.Epoch{Time: 869566202, Level: 2}, n: 869566199, x: 3}, testG{e: lookup.Epoch{Time: 851878045, Level: 5}, n: 851878045, x: 4}, testG{e: lookup.Epoch{Time: 824469671, Level: 12}, n: 824466504, x: 13}, testG{e: lookup.Epoch{Time: 819830223, Level: 9}, n: 816550241, x: 22}, testG{e: lookup.Epoch{Time: 813720249, Level: 20}, n: 801351581, x: 28}, testG{e: lookup.Epoch{Time: 831200185, Level: 20}, n: 830760165, x: 19}, testG{e: lookup.Epoch{Time: 838915973, Level: 9}, n: 838915972, x: 8}, testG{e: lookup.Epoch{Time: 812902644, Level: 5}, n: 812902644, x: 4}, testG{e: lookup.Epoch{Time: 812755887, Level: 3}, n: 812755887, x: 2}, testG{e: lookup.Epoch{Time: 822497779, Level: 8}, n: 822486000, x: 14}, testG{e: lookup.Epoch{Time: 832407585, Level: 9}, n: 579450238, x: 28}, testG{e: lookup.Epoch{Time: 799645403, Level: 23}, n: 799645403, x: 22}, testG{e: lookup.Epoch{Time: 827279665, Level: 2}, n: 826723872, x: 19}, testG{e: lookup.Epoch{Time: 846062554, Level: 6}, n: 765881119, x: 28}, testG{e: lookup.Epoch{Time: 855122998, Level: 6}, n: 855122978, x: 5}, testG{e: lookup.Epoch{Time: 841905104, Level: 4}, n: 751401236, x: 28}, testG{e: lookup.Epoch{Time: 857737438, Level: 12}, n: 325468127, x: 29}, testG{e: lookup.Epoch{Time: 838103691, Level: 18}, n: 779030823, x: 28}, testG{e: lookup.Epoch{Time: 841581240, Level: 22}, n: 841581239, x: 21}} +var testGetNextLevelCases = []testG{{e: lookup.Epoch{Time: 989875233, Level: 12}, n: 989807323, x: 24}, {e: lookup.Epoch{Time: 995807650, Level: 18}, n: 995807649, x: 17}, {e: lookup.Epoch{Time: 969167082, Level: 0}, n: 969111431, x: 18}, {e: lookup.Epoch{Time: 993087628, Level: 14}, n: 993087627, x: 13}, {e: lookup.Epoch{Time: 963364631, Level: 20}, n: 962941578, x: 19}, {e: lookup.Epoch{Time: 963497510, Level: 16}, n: 963497509, x: 15}, {e: lookup.Epoch{Time: 955421349, Level: 22}, n: 929292183, x: 27}, {e: lookup.Epoch{Time: 968220379, Level: 15}, n: 968220378, x: 14}, {e: lookup.Epoch{Time: 939129014, Level: 6}, n: 939126953, x: 11}, {e: lookup.Epoch{Time: 907847903, Level: 6}, n: 907846146, x: 11}, {e: lookup.Epoch{Time: 910835564, Level: 15}, n: 703619757, x: 28}, {e: lookup.Epoch{Time: 913578333, Level: 22}, n: 913578332, x: 21}, {e: lookup.Epoch{Time: 895818460, Level: 3}, n: 895818132, x: 9}, {e: lookup.Epoch{Time: 903843025, Level: 24}, n: 903843025, x: 23}, {e: lookup.Epoch{Time: 877889433, Level: 13}, n: 149120378, x: 29}, {e: lookup.Epoch{Time: 901450396, Level: 10}, n: 858997793, x: 26}, {e: lookup.Epoch{Time: 925179910, Level: 3}, n: 925177237, x: 13}, {e: lookup.Epoch{Time: 913485477, Level: 21}, n: 907146511, x: 22}, {e: lookup.Epoch{Time: 924462991, Level: 18}, n: 924462990, x: 17}, {e: lookup.Epoch{Time: 941175128, Level: 13}, n: 941168924, x: 13}, {e: lookup.Epoch{Time: 920126583, Level: 3}, n: 538054817, x: 28}, {e: lookup.Epoch{Time: 891721312, Level: 18}, n: 890975671, x: 21}, {e: lookup.Epoch{Time: 920397342, Level: 11}, n: 920396960, x: 10}, {e: lookup.Epoch{Time: 953406530, Level: 3}, n: 953406530, x: 2}, {e: lookup.Epoch{Time: 920024527, Level: 23}, n: 920024527, x: 22}, {e: lookup.Epoch{Time: 927050922, Level: 7}, n: 927049632, x: 11}, {e: lookup.Epoch{Time: 894599900, Level: 10}, n: 890021707, x: 22}, {e: lookup.Epoch{Time: 883010150, Level: 3}, n: 882969902, x: 15}, {e: lookup.Epoch{Time: 855561102, Level: 22}, n: 855561102, x: 21}, {e: lookup.Epoch{Time: 828245477, Level: 19}, n: 825245571, x: 22}, {e: lookup.Epoch{Time: 851095026, Level: 4}, n: 851083702, x: 13}, {e: lookup.Epoch{Time: 879209039, Level: 11}, n: 879209039, x: 10}, {e: lookup.Epoch{Time: 859265651, Level: 0}, n: 840582083, x: 24}, {e: lookup.Epoch{Time: 827349870, Level: 24}, n: 827349869, x: 23}, {e: lookup.Epoch{Time: 819602318, Level: 3}, n: 18446744073490860182, x: 31}, {e: lookup.Epoch{Time: 849708538, Level: 7}, n: 849708538, x: 6}, {e: lookup.Epoch{Time: 873885094, Level: 11}, n: 873881798, x: 11}, {e: lookup.Epoch{Time: 852169070, Level: 1}, n: 852049399, x: 17}, {e: lookup.Epoch{Time: 852885343, Level: 8}, n: 852875652, x: 13}, {e: lookup.Epoch{Time: 830957057, Level: 8}, n: 830955867, x: 10}, {e: lookup.Epoch{Time: 807353611, Level: 4}, n: 807325211, x: 16}, {e: lookup.Epoch{Time: 803198793, Level: 8}, n: 696477575, x: 26}, {e: lookup.Epoch{Time: 791356887, Level: 10}, n: 791356003, x: 10}, {e: lookup.Epoch{Time: 817771215, Level: 12}, n: 817708431, x: 17}, {e: lookup.Epoch{Time: 846211146, Level: 14}, n: 846211146, x: 13}, {e: lookup.Epoch{Time: 821849822, Level: 9}, n: 821849229, x: 9}, {e: lookup.Epoch{Time: 789508756, Level: 9}, n: 789508755, x: 8}, {e: lookup.Epoch{Time: 814088521, Level: 12}, n: 814088512, x: 11}, {e: lookup.Epoch{Time: 813665673, Level: 6}, n: 813548257, x: 17}, {e: lookup.Epoch{Time: 791472209, Level: 6}, n: 720857845, x: 26}, {e: lookup.Epoch{Time: 805687744, Level: 2}, n: 805687720, x: 6}, {e: lookup.Epoch{Time: 783153927, Level: 12}, n: 783134053, x: 14}, {e: lookup.Epoch{Time: 815033655, Level: 11}, n: 815033654, x: 10}, {e: lookup.Epoch{Time: 821184581, Level: 6}, n: 821184464, x: 11}, {e: lookup.Epoch{Time: 841908114, Level: 2}, n: 841636025, x: 18}, {e: lookup.Epoch{Time: 862969167, Level: 20}, n: 862919955, x: 19}, {e: lookup.Epoch{Time: 887604565, Level: 21}, n: 887604564, x: 20}, {e: lookup.Epoch{Time: 863723789, Level: 10}, n: 858274530, x: 22}, {e: lookup.Epoch{Time: 851533290, Level: 10}, n: 851531385, x: 11}, {e: lookup.Epoch{Time: 826032484, Level: 14}, n: 826032484, x: 13}, {e: lookup.Epoch{Time: 819401505, Level: 7}, n: 818943526, x: 18}, {e: lookup.Epoch{Time: 800886832, Level: 12}, n: 800563106, x: 19}, {e: lookup.Epoch{Time: 780767476, Level: 10}, n: 694450997, x: 26}, {e: lookup.Epoch{Time: 789209418, Level: 15}, n: 789209417, x: 14}, {e: lookup.Epoch{Time: 816086666, Level: 9}, n: 816034646, x: 18}, {e: lookup.Epoch{Time: 835407077, Level: 21}, n: 835407076, x: 20}, {e: lookup.Epoch{Time: 846527322, Level: 20}, n: 846527321, x: 19}, {e: lookup.Epoch{Time: 850131130, Level: 19}, n: 18446744073670013406, x: 31}, {e: lookup.Epoch{Time: 842248607, Level: 24}, n: 783963834, x: 28}, {e: lookup.Epoch{Time: 816181999, Level: 2}, n: 816124867, x: 15}, {e: lookup.Epoch{Time: 806627026, Level: 17}, n: 756013427, x: 28}, {e: lookup.Epoch{Time: 826223084, Level: 4}, n: 826169865, x: 16}, {e: lookup.Epoch{Time: 835380147, Level: 21}, n: 835380147, x: 20}, {e: lookup.Epoch{Time: 860137874, Level: 3}, n: 860137782, x: 7}, {e: lookup.Epoch{Time: 860623757, Level: 8}, n: 860621582, x: 12}, {e: lookup.Epoch{Time: 875464114, Level: 24}, n: 875464114, x: 23}, {e: lookup.Epoch{Time: 853804052, Level: 6}, n: 853804051, x: 5}, {e: lookup.Epoch{Time: 864150903, Level: 14}, n: 854360673, x: 24}, {e: lookup.Epoch{Time: 850104561, Level: 23}, n: 850104561, x: 22}, {e: lookup.Epoch{Time: 878020186, Level: 24}, n: 878020186, x: 23}, {e: lookup.Epoch{Time: 900150940, Level: 8}, n: 899224760, x: 21}, {e: lookup.Epoch{Time: 869566202, Level: 2}, n: 869566199, x: 3}, {e: lookup.Epoch{Time: 851878045, Level: 5}, n: 851878045, x: 4}, {e: lookup.Epoch{Time: 824469671, Level: 12}, n: 824466504, x: 13}, {e: lookup.Epoch{Time: 819830223, Level: 9}, n: 816550241, x: 22}, {e: lookup.Epoch{Time: 813720249, Level: 20}, n: 801351581, x: 28}, {e: lookup.Epoch{Time: 831200185, Level: 20}, n: 830760165, x: 19}, {e: lookup.Epoch{Time: 838915973, Level: 9}, n: 838915972, x: 8}, {e: lookup.Epoch{Time: 812902644, Level: 5}, n: 812902644, x: 4}, {e: lookup.Epoch{Time: 812755887, Level: 3}, n: 812755887, x: 2}, {e: lookup.Epoch{Time: 822497779, Level: 8}, n: 822486000, x: 14}, {e: lookup.Epoch{Time: 832407585, Level: 9}, n: 579450238, x: 28}, {e: lookup.Epoch{Time: 799645403, Level: 23}, n: 799645403, x: 22}, {e: lookup.Epoch{Time: 827279665, Level: 2}, n: 826723872, x: 19}, {e: lookup.Epoch{Time: 846062554, Level: 6}, n: 765881119, x: 28}, {e: lookup.Epoch{Time: 855122998, Level: 6}, n: 855122978, x: 5}, {e: lookup.Epoch{Time: 841905104, Level: 4}, n: 751401236, x: 28}, {e: lookup.Epoch{Time: 857737438, Level: 12}, n: 325468127, x: 29}, {e: lookup.Epoch{Time: 838103691, Level: 18}, n: 779030823, x: 28}, {e: lookup.Epoch{Time: 841581240, Level: 22}, n: 841581239, x: 21}} // TestGetNextLevel tests the lookup.GetNextLevel function func TestGetNextLevel(t *testing.T) { From f35975ea21d4018ea5dccfdce3c420de5b5f5140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 17 May 2019 01:45:05 +0300 Subject: [PATCH 29/34] core/rawdb, eth/downloader: align 64bit atomic fields --- core/rawdb/freezer.go | 6 +++++- core/rawdb/freezer_table.go | 6 +++++- eth/downloader/downloader.go | 10 +++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 67ed87d66c..33da53787a 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -66,8 +66,12 @@ const ( // reserving it for go-ethereum. This would also reduce the memory requirements // of Geth, and thus also GC overhead. type freezer struct { + // WARNING: The `frozen` field is accessed atomically. On 32 bit platforms, only + // 64-bit aligned fields can be atomic. The struct is guaranteed to be so aligned, + // so take advantage of that (https://golang.org/pkg/sync/atomic/#pkg-note-BUG). + frozen uint64 // Number of blocks already frozen + tables map[string]*freezerTable // Data tables for storing everything - frozen uint64 // Number of blocks already frozen instanceLock fileutil.Releaser // File-system lock to prevent double opens } diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index 673a181e49..d1ece414bb 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -73,6 +73,11 @@ func (i *indexEntry) marshallBinary() []byte { // It consists of a data file (snappy encoded arbitrary data blobs) and an indexEntry // file (uncompressed 64 bit indices into the data file). type freezerTable struct { + // WARNING: The `items` field is accessed atomically. On 32 bit platforms, only + // 64-bit aligned fields can be atomic. The struct is guaranteed to be so aligned, + // so take advantage of that (https://golang.org/pkg/sync/atomic/#pkg-note-BUG). + items uint64 // Number of items stored in the table (including items removed from tail) + noCompression bool // if true, disables snappy compression. Note: does not work retroactively maxFileSize uint32 // Max file size for data-files name string @@ -86,7 +91,6 @@ type freezerTable struct { // In the case that old items are deleted (from the tail), we use itemOffset // to count how many historic items have gone missing. - items uint64 // Number of items stored in the table (including items removed from tail) itemOffset uint32 // Offset (number of discarded items) headBytes uint32 // Number of bytes written to the head file diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 495fa0e74d..321c4473b6 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -98,6 +98,13 @@ var ( ) type Downloader struct { + // WARNING: The `rttEstimate` and `rttConfidence` fields are accessed atomically. + // On 32 bit platforms, only 64-bit aligned fields can be atomic. The struct is + // guaranteed to be so aligned, so take advantage of that. For more information, + // see https://golang.org/pkg/sync/atomic/#pkg-note-BUG. + rttEstimate uint64 // Round trip time to target for download requests + rttConfidence uint64 // Confidence in the estimated RTT (unit: millionths to allow atomic ops) + mode SyncMode // Synchronisation mode defining the strategy used (per sync cycle) mux *event.TypeMux // Event multiplexer to announce sync operation events @@ -109,9 +116,6 @@ type Downloader struct { stateDB ethdb.Database // Database to state sync into (and deduplicate via) stateBloom *trie.SyncBloom // Bloom filter for fast trie node existence checks - rttEstimate uint64 // Round trip time to target for download requests - rttConfidence uint64 // Confidence in the estimated RTT (unit: millionths to allow atomic ops) - // Statistics syncStatsChainOrigin uint64 // Origin block number where syncing started at syncStatsChainHeight uint64 // Highest block number known when syncing started From 8cce620311cdd17f388e8d258a02eb8ccb35adb5 Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Fri, 17 May 2019 01:06:20 +0200 Subject: [PATCH 30/34] build: disable swarm packages (#19585) * build: disable swarm packages * build: remove allCrossCompiledArchiveFiles; inline allToolsArchiveFiles * build: get rid of some superfluous comments --- build/ci.go | 38 ++++---------------------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/build/ci.go b/build/ci.go index dbf158005d..50433c892b 100644 --- a/build/ci.go +++ b/build/ci.go @@ -60,7 +60,6 @@ import ( "github.com/ethereum/go-ethereum/internal/build" "github.com/ethereum/go-ethereum/params" - sv "github.com/ethereum/go-ethereum/swarm/version" ) var ( @@ -83,12 +82,6 @@ var ( executablePath("clef"), } - // Files that end up in the swarm*.zip archive. - swarmArchiveFiles = []string{ - "COPYING", - executablePath("swarm"), - } - // A debian package is created for all executables listed here. debExecutables = []debExecutable{ { @@ -126,13 +119,6 @@ var ( } // A debian package is created for all executables listed here. - debSwarmExecutables = []debExecutable{ - { - BinaryName: "swarm", - PackageName: "ethereum-swarm", - Description: "Ethereum Swarm daemon and tools", - }, - } debEthereum = debPackage{ Name: "ethereum", @@ -140,21 +126,11 @@ var ( Executables: debExecutables, } - debSwarm = debPackage{ - Name: "ethereum-swarm", - Version: sv.Version, - Executables: debSwarmExecutables, - } - // Debian meta packages to build and push to Ubuntu PPA debPackages = []debPackage{ - debSwarm, debEthereum, } - // Packages to be cross-compiled by the xgo command - allCrossCompiledArchiveFiles = append(allToolsArchiveFiles, swarmArchiveFiles...) - // Distros for which packages are created. // Note: vivid is unsupported because there is no golang-1.6 package for it. // Note: wily is unsupported because it was officially deprecated on lanchpad. @@ -409,9 +385,6 @@ func doArchive(cmdline []string) { basegeth = archiveBasename(*arch, params.ArchiveVersion(env.Commit)) geth = "geth-" + basegeth + ext alltools = "geth-alltools-" + basegeth + ext - - baseswarm = archiveBasename(*arch, sv.ArchiveVersion(env.Commit)) - swarm = "swarm-" + baseswarm + ext ) maybeSkipArchive(env) if err := build.WriteArchive(geth, gethArchiveFiles); err != nil { @@ -420,10 +393,7 @@ func doArchive(cmdline []string) { if err := build.WriteArchive(alltools, allToolsArchiveFiles); err != nil { log.Fatal(err) } - if err := build.WriteArchive(swarm, swarmArchiveFiles); err != nil { - log.Fatal(err) - } - for _, archive := range []string{geth, alltools, swarm} { + for _, archive := range []string{geth, alltools} { if err := archiveUpload(archive, *upload, *signer); err != nil { log.Fatal(err) } @@ -586,8 +556,8 @@ func isUnstableBuild(env build.Environment) bool { } type debPackage struct { - Name string // the name of the Debian package to produce, e.g. "ethereum", or "ethereum-swarm" - Version string // the clean version of the debPackage, e.g. 1.8.12 or 0.3.0, without any metadata + Name string // the name of the Debian package to produce, e.g. "ethereum" + Version string // the clean version of the debPackage, e.g. 1.8.12, without any metadata Executables []debExecutable // executables to be included in the package } @@ -1023,7 +993,7 @@ func doXgo(cmdline []string) { if *alltools { args = append(args, []string{"--dest", GOBIN}...) - for _, res := range allCrossCompiledArchiveFiles { + for _, res := range allToolsArchiveFiles { if strings.HasPrefix(res, GOBIN) { // Binary tool found, cross build it explicitly args = append(args, "./"+filepath.Join("cmd", filepath.Base(res))) From c3b317a4fc5e59da87b2bd2b858c038cfb4ba07a Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Fri, 17 May 2019 10:12:03 +0200 Subject: [PATCH 31/34] swarm/version: v0.4.0 stable (#19586) --- swarm/version/version.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/swarm/version/version.go b/swarm/version/version.go index b4a1c9a981..0a5af222c8 100644 --- a/swarm/version/version.go +++ b/swarm/version/version.go @@ -21,10 +21,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 3 // Minor version component of the current release - VersionPatch = 16 // Patch version component of the current release - VersionMeta = "unstable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 4 // Minor version component of the current release + VersionPatch = 0 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) // Version holds the textual version string. From 509facd631e646989dae7fc9c3ec11a05b11a800 Mon Sep 17 00:00:00 2001 From: Anton Evangelatov Date: Fri, 17 May 2019 10:14:39 +0200 Subject: [PATCH 32/34] swarm/version: v0.4.1 unstable (#19587) --- swarm/version/version.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/swarm/version/version.go b/swarm/version/version.go index 0a5af222c8..271a9cede4 100644 --- a/swarm/version/version.go +++ b/swarm/version/version.go @@ -21,10 +21,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 4 // Minor version component of the current release - VersionPatch = 0 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 4 // Minor version component of the current release + VersionPatch = 1 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version holds the textual version string. From e687d063c360a78786e021f5ada935ef643e3815 Mon Sep 17 00:00:00 2001 From: manlio Date: Fri, 17 May 2019 14:24:04 +0100 Subject: [PATCH 33/34] accounts/abi: fix TestUnpackMethodIntoMap (#19484) --- accounts/abi/abi_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/abi/abi_test.go b/accounts/abi/abi_test.go index 42b60a6395..60fe104574 100644 --- a/accounts/abi/abi_test.go +++ b/accounts/abi/abi_test.go @@ -785,7 +785,7 @@ func TestUnpackMethodIntoMap(t *testing.T) { if err = abi.UnpackIntoMap(getMap, "get", data); err != nil { t.Error(err) } - if len(sendMap) != 1 { + if len(getMap) != 1 { t.Error("unpacked `get` map expected to have length 1") } expectedBytes := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 88, 0} From 97d3615612a49488d02807944696d001b88f9e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felf=C3=B6ldi=20Zsolt?= Date: Fri, 17 May 2019 20:39:39 +0200 Subject: [PATCH 34/34] les: avoid fetcher deadlock on requestChn (#19571) * les: avoid fetcher deadlock on requestChn --- les/fetcher.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/les/fetcher.go b/les/fetcher.go index 057552f532..fa02be9a20 100644 --- a/les/fetcher.go +++ b/les/fetcher.go @@ -55,7 +55,8 @@ type lightFetcher struct { requested map[uint64]fetchRequest deliverChn chan fetchResponse timeoutChn chan uint64 - requestChn chan bool // true if initiated from outside + requestTriggered bool + requestTrigger chan struct{} lastTrustedHeader *types.Header } @@ -122,7 +123,7 @@ func newLightFetcher(pm *ProtocolManager) *lightFetcher { deliverChn: make(chan fetchResponse, 100), requested: make(map[uint64]fetchRequest), timeoutChn: make(chan uint64), - requestChn: make(chan bool, 100), + requestTrigger: make(chan struct{}, 1), syncDone: make(chan *peer), maxConfirmedTd: big.NewInt(0), } @@ -135,31 +136,26 @@ func newLightFetcher(pm *ProtocolManager) *lightFetcher { // syncLoop is the main event loop of the light fetcher func (f *lightFetcher) syncLoop() { - requesting := false defer f.pm.wg.Done() for { select { case <-f.pm.quitSync: return - // when a new announce is received, request loop keeps running until - // no further requests are necessary or possible - case newAnnounce := <-f.requestChn: + // request loop keeps running until no further requests are necessary or possible + case <-f.requestTrigger: f.lock.Lock() - s := requesting - requesting = false var ( rq *distReq reqID uint64 syncing bool ) - - if !f.syncing && !(newAnnounce && s) { + if !f.syncing { rq, reqID, syncing = f.nextRequest() } + f.requestTriggered = rq != nil f.lock.Unlock() if rq != nil { - requesting = true if _, ok := <-f.pm.reqDist.queue(rq); ok { if syncing { f.lock.Lock() @@ -176,11 +172,11 @@ func (f *lightFetcher) syncLoop() { } f.reqMu.Unlock() // keep starting new requests while possible - f.requestChn <- false + f.requestTrigger <- struct{}{} }() } } else { - f.requestChn <- false + f.requestTrigger <- struct{}{} } } case reqID := <-f.timeoutChn: @@ -220,7 +216,7 @@ func (f *lightFetcher) syncLoop() { f.checkSyncedHeaders(p) f.syncing = false f.lock.Unlock() - f.requestChn <- false + f.requestTrigger <- struct{}{} // f.requestTriggered is always true here } } } @@ -354,7 +350,10 @@ func (f *lightFetcher) announce(p *peer, head *announceData) { fp.lastAnnounced = n p.lock.Unlock() f.checkUpdateStats(p, nil) - f.requestChn <- true + if !f.requestTriggered { + f.requestTriggered = true + f.requestTrigger <- struct{}{} + } } // peerHasBlock returns true if we can assume the peer knows the given block