mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
Delete core directory
Signed-off-by: Isabel Schöps Thiel @IsabelSchoepd <155141998+IST-Github@users.noreply.github.com>
This commit is contained in:
parent
e6e2889fb7
commit
35db0e49c3
267 changed files with 0 additions and 83111 deletions
12
core/.gitignore
vendored
12
core/.gitignore
vendored
|
|
@ -1,12 +0,0 @@
|
||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
#
|
|
||||||
# If you find yourself ignoring temporary files generated by your text editor
|
|
||||||
# or operating system, you probably want to add a global ignore instead:
|
|
||||||
# git config --global core.excludesfile ~/.gitignore_global
|
|
||||||
|
|
||||||
/tmp
|
|
||||||
*/**/*un~
|
|
||||||
*un~
|
|
||||||
.DS_Store
|
|
||||||
*/**/.DS_Store
|
|
||||||
|
|
||||||
136
core/asm/asm.go
136
core/asm/asm.go
|
|
@ -1,136 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package asm provides support for dealing with EVM assembly instructions (e.g., disassembling them).
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Iterator for disassembled EVM instructions
|
|
||||||
type instructionIterator struct {
|
|
||||||
code []byte
|
|
||||||
pc uint64
|
|
||||||
arg []byte
|
|
||||||
op vm.OpCode
|
|
||||||
error error
|
|
||||||
started bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInstructionIterator creates a new instruction iterator.
|
|
||||||
func NewInstructionIterator(code []byte) *instructionIterator {
|
|
||||||
it := new(instructionIterator)
|
|
||||||
it.code = code
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next returns true if there is a next instruction and moves on.
|
|
||||||
func (it *instructionIterator) Next() bool {
|
|
||||||
if it.error != nil || uint64(len(it.code)) <= it.pc {
|
|
||||||
// We previously reached an error or the end.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if it.started {
|
|
||||||
// Since the iteration has been already started we move to the next instruction.
|
|
||||||
if it.arg != nil {
|
|
||||||
it.pc += uint64(len(it.arg))
|
|
||||||
}
|
|
||||||
it.pc++
|
|
||||||
} else {
|
|
||||||
// We start the iteration from the first instruction.
|
|
||||||
it.started = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if uint64(len(it.code)) <= it.pc {
|
|
||||||
// We reached the end.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
it.op = vm.OpCode(it.code[it.pc])
|
|
||||||
if it.op.IsPush() {
|
|
||||||
a := uint64(it.op) - uint64(vm.PUSH1) + 1
|
|
||||||
u := it.pc + 1 + a
|
|
||||||
if uint64(len(it.code)) <= it.pc || uint64(len(it.code)) < u {
|
|
||||||
it.error = fmt.Errorf("incomplete push instruction at %v", it.pc)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
it.arg = it.code[it.pc+1 : u]
|
|
||||||
} else {
|
|
||||||
it.arg = nil
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns any error that may have been encountered.
|
|
||||||
func (it *instructionIterator) Error() error {
|
|
||||||
return it.error
|
|
||||||
}
|
|
||||||
|
|
||||||
// PC returns the PC of the current instruction.
|
|
||||||
func (it *instructionIterator) PC() uint64 {
|
|
||||||
return it.pc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Op returns the opcode of the current instruction.
|
|
||||||
func (it *instructionIterator) Op() vm.OpCode {
|
|
||||||
return it.op
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arg returns the argument of the current instruction.
|
|
||||||
func (it *instructionIterator) Arg() []byte {
|
|
||||||
return it.arg
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintDisassembled pretty-print all disassembled EVM instructions to stdout.
|
|
||||||
func PrintDisassembled(code string) error {
|
|
||||||
script, err := hex.DecodeString(code)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
it := NewInstructionIterator(script)
|
|
||||||
for it.Next() {
|
|
||||||
if it.Arg() != nil && 0 < len(it.Arg()) {
|
|
||||||
fmt.Printf("%05x: %v %#x\n", it.PC(), it.Op(), it.Arg())
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%05x: %v\n", it.PC(), it.Op())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return it.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disassemble returns all disassembled EVM instructions in human-readable format.
|
|
||||||
func Disassemble(script []byte) ([]string, error) {
|
|
||||||
instrs := make([]string, 0)
|
|
||||||
|
|
||||||
it := NewInstructionIterator(script)
|
|
||||||
for it.Next() {
|
|
||||||
if it.Arg() != nil && 0 < len(it.Arg()) {
|
|
||||||
instrs = append(instrs, fmt.Sprintf("%05x: %v %#x\n", it.PC(), it.Op(), it.Arg()))
|
|
||||||
} else {
|
|
||||||
instrs = append(instrs, fmt.Sprintf("%05x: %v\n", it.PC(), it.Op()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := it.Error(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return instrs, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"encoding/hex"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests disassembling instructions
|
|
||||||
func TestInstructionIterator(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
want int
|
|
||||||
code string
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{2, "61000000", ""}, // valid code
|
|
||||||
{0, "6100", "incomplete push instruction at 0"}, // invalid code
|
|
||||||
{2, "5900", ""}, // push0
|
|
||||||
{0, "", ""}, // empty
|
|
||||||
|
|
||||||
} {
|
|
||||||
var (
|
|
||||||
have int
|
|
||||||
code, _ = hex.DecodeString(tc.code)
|
|
||||||
it = NewInstructionIterator(code)
|
|
||||||
)
|
|
||||||
for it.Next() {
|
|
||||||
have++
|
|
||||||
}
|
|
||||||
var haveErr = ""
|
|
||||||
if it.Error() != nil {
|
|
||||||
haveErr = it.Error().Error()
|
|
||||||
}
|
|
||||||
if haveErr != tc.wantErr {
|
|
||||||
t.Errorf("test %d: encountered error: %q want %q", i, haveErr, tc.wantErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if have != tc.want {
|
|
||||||
t.Errorf("wrong instruction count, have %d want %d", have, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Compiler contains information about the parsed source
|
|
||||||
// and holds the tokens for the program.
|
|
||||||
type Compiler struct {
|
|
||||||
tokens []token
|
|
||||||
out []byte
|
|
||||||
|
|
||||||
labels map[string]int
|
|
||||||
|
|
||||||
pc, pos int
|
|
||||||
|
|
||||||
debug bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCompiler returns a new allocated compiler.
|
|
||||||
func NewCompiler(debug bool) *Compiler {
|
|
||||||
return &Compiler{
|
|
||||||
labels: make(map[string]int),
|
|
||||||
debug: debug,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feed feeds tokens into ch and are interpreted by
|
|
||||||
// the compiler.
|
|
||||||
//
|
|
||||||
// feed is the first pass in the compile stage as it collects the used labels in the
|
|
||||||
// program and keeps a program counter which is used to determine the locations of the
|
|
||||||
// jump dests. The labels can than be used in the second stage to push labels and
|
|
||||||
// determine the right position.
|
|
||||||
func (c *Compiler) Feed(ch <-chan token) {
|
|
||||||
var prev token
|
|
||||||
for i := range ch {
|
|
||||||
switch i.typ {
|
|
||||||
case number:
|
|
||||||
num := math.MustParseBig256(i.text).Bytes()
|
|
||||||
if len(num) == 0 {
|
|
||||||
num = []byte{0}
|
|
||||||
}
|
|
||||||
c.pc += len(num)
|
|
||||||
case stringValue:
|
|
||||||
c.pc += len(i.text) - 2
|
|
||||||
case element:
|
|
||||||
c.pc++
|
|
||||||
case labelDef:
|
|
||||||
c.labels[i.text] = c.pc
|
|
||||||
c.pc++
|
|
||||||
case label:
|
|
||||||
c.pc += 4
|
|
||||||
if prev.typ == element && isJump(prev.text) {
|
|
||||||
c.pc++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.tokens = append(c.tokens, i)
|
|
||||||
prev = i
|
|
||||||
}
|
|
||||||
if c.debug {
|
|
||||||
fmt.Fprintln(os.Stderr, "found", len(c.labels), "labels")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile compiles the current tokens and returns a binary string that can be interpreted
|
|
||||||
// by the EVM and an error if it failed.
|
|
||||||
//
|
|
||||||
// compile is the second stage in the compile phase which compiles the tokens to EVM
|
|
||||||
// instructions.
|
|
||||||
func (c *Compiler) Compile() (string, []error) {
|
|
||||||
var errors []error
|
|
||||||
// continue looping over the tokens until
|
|
||||||
// the stack has been exhausted.
|
|
||||||
for c.pos < len(c.tokens) {
|
|
||||||
if err := c.compileLine(); err != nil {
|
|
||||||
errors = append(errors, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// turn the binary to hex
|
|
||||||
h := hex.EncodeToString(c.out)
|
|
||||||
return h, errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns the next token and increments the
|
|
||||||
// position.
|
|
||||||
func (c *Compiler) next() token {
|
|
||||||
token := c.tokens[c.pos]
|
|
||||||
c.pos++
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
// compileLine compiles a single line instruction e.g.
|
|
||||||
// "push 1", "jump @label".
|
|
||||||
func (c *Compiler) compileLine() error {
|
|
||||||
n := c.next()
|
|
||||||
if n.typ != lineStart {
|
|
||||||
return compileErr(n, n.typ.String(), lineStart.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
lvalue := c.next()
|
|
||||||
switch lvalue.typ {
|
|
||||||
case eof:
|
|
||||||
return nil
|
|
||||||
case element:
|
|
||||||
if err := c.compileElement(lvalue); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case labelDef:
|
|
||||||
c.compileLabel()
|
|
||||||
case lineEnd:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return compileErr(lvalue, lvalue.text, fmt.Sprintf("%v or %v", labelDef, element))
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := c.next(); n.typ != lineEnd {
|
|
||||||
return compileErr(n, n.text, lineEnd.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseNumber compiles the number to bytes
|
|
||||||
func parseNumber(tok token) ([]byte, error) {
|
|
||||||
if tok.typ != number {
|
|
||||||
panic("parseNumber of non-number token")
|
|
||||||
}
|
|
||||||
num, ok := math.ParseBig256(tok.text)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("invalid number")
|
|
||||||
}
|
|
||||||
bytes := num.Bytes()
|
|
||||||
if len(bytes) == 0 {
|
|
||||||
bytes = []byte{0}
|
|
||||||
}
|
|
||||||
return bytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// compileElement compiles the element (push & label or both)
|
|
||||||
// to a binary representation and may error if incorrect statements
|
|
||||||
// where fed.
|
|
||||||
func (c *Compiler) compileElement(element token) error {
|
|
||||||
switch {
|
|
||||||
case isJump(element.text):
|
|
||||||
return c.compileJump(element.text)
|
|
||||||
case isPush(element.text):
|
|
||||||
return c.compilePush()
|
|
||||||
default:
|
|
||||||
c.outputOpcode(toBinary(element.text))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Compiler) compileJump(jumpType string) error {
|
|
||||||
rvalue := c.next()
|
|
||||||
switch rvalue.typ {
|
|
||||||
case number:
|
|
||||||
numBytes, err := parseNumber(rvalue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.outputBytes(numBytes)
|
|
||||||
|
|
||||||
case stringValue:
|
|
||||||
// strings are quoted, remove them.
|
|
||||||
str := rvalue.text[1 : len(rvalue.text)-2]
|
|
||||||
c.outputBytes([]byte(str))
|
|
||||||
|
|
||||||
case label:
|
|
||||||
c.outputOpcode(vm.PUSH4)
|
|
||||||
pos := big.NewInt(int64(c.labels[rvalue.text])).Bytes()
|
|
||||||
pos = append(make([]byte, 4-len(pos)), pos...)
|
|
||||||
c.outputBytes(pos)
|
|
||||||
|
|
||||||
case lineEnd:
|
|
||||||
// push without argument is supported, it just takes the destination from the stack.
|
|
||||||
c.pos--
|
|
||||||
|
|
||||||
default:
|
|
||||||
return compileErr(rvalue, rvalue.text, "number, string or label")
|
|
||||||
}
|
|
||||||
// push the operation
|
|
||||||
c.outputOpcode(toBinary(jumpType))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Compiler) compilePush() error {
|
|
||||||
// handle pushes. pushes are read from left to right.
|
|
||||||
var value []byte
|
|
||||||
rvalue := c.next()
|
|
||||||
switch rvalue.typ {
|
|
||||||
case number:
|
|
||||||
value = math.MustParseBig256(rvalue.text).Bytes()
|
|
||||||
if len(value) == 0 {
|
|
||||||
value = []byte{0}
|
|
||||||
}
|
|
||||||
case stringValue:
|
|
||||||
value = []byte(rvalue.text[1 : len(rvalue.text)-1])
|
|
||||||
case label:
|
|
||||||
value = big.NewInt(int64(c.labels[rvalue.text])).Bytes()
|
|
||||||
value = append(make([]byte, 4-len(value)), value...)
|
|
||||||
default:
|
|
||||||
return compileErr(rvalue, rvalue.text, "number, string or label")
|
|
||||||
}
|
|
||||||
if len(value) > 32 {
|
|
||||||
return fmt.Errorf("%d: string or number size > 32 bytes", rvalue.lineno+1)
|
|
||||||
}
|
|
||||||
c.outputOpcode(vm.OpCode(int(vm.PUSH1) - 1 + len(value)))
|
|
||||||
c.outputBytes(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// compileLabel pushes a jumpdest to the binary slice.
|
|
||||||
func (c *Compiler) compileLabel() {
|
|
||||||
c.outputOpcode(vm.JUMPDEST)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Compiler) outputOpcode(op vm.OpCode) {
|
|
||||||
if c.debug {
|
|
||||||
fmt.Printf("%d: %v\n", len(c.out), op)
|
|
||||||
}
|
|
||||||
c.out = append(c.out, byte(op))
|
|
||||||
}
|
|
||||||
|
|
||||||
// output pushes the value v to the binary stack.
|
|
||||||
func (c *Compiler) outputBytes(b []byte) {
|
|
||||||
if c.debug {
|
|
||||||
fmt.Printf("%d: %x\n", len(c.out), b)
|
|
||||||
}
|
|
||||||
c.out = append(c.out, b...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPush returns whether the string op is either any of
|
|
||||||
// push(N).
|
|
||||||
func isPush(op string) bool {
|
|
||||||
return strings.EqualFold(op, "PUSH")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isJump returns whether the string op is jump(i)
|
|
||||||
func isJump(op string) bool {
|
|
||||||
return strings.EqualFold(op, "JUMPI") || strings.EqualFold(op, "JUMP")
|
|
||||||
}
|
|
||||||
|
|
||||||
// toBinary converts text to a vm.OpCode
|
|
||||||
func toBinary(text string) vm.OpCode {
|
|
||||||
return vm.StringToOp(strings.ToUpper(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
type compileError struct {
|
|
||||||
got string
|
|
||||||
want string
|
|
||||||
|
|
||||||
lineno int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err compileError) Error() string {
|
|
||||||
return fmt.Sprintf("%d: syntax error: unexpected %v, expected %v", err.lineno, err.got, err.want)
|
|
||||||
}
|
|
||||||
|
|
||||||
func compileErr(c token, got, want string) error {
|
|
||||||
return compileError{
|
|
||||||
got: got,
|
|
||||||
want: want,
|
|
||||||
lineno: c.lineno + 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCompiler(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input, output string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
GAS
|
|
||||||
label:
|
|
||||||
PUSH @label
|
|
||||||
`,
|
|
||||||
output: "5a5b6300000001",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
PUSH @label
|
|
||||||
label:
|
|
||||||
`,
|
|
||||||
output: "63000000055b",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
PUSH @label
|
|
||||||
JUMP
|
|
||||||
label:
|
|
||||||
`,
|
|
||||||
output: "6300000006565b",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
JUMP @label
|
|
||||||
label:
|
|
||||||
`,
|
|
||||||
output: "6300000006565b",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
JUMP @label
|
|
||||||
label: ;; comment
|
|
||||||
ADD ;; comment
|
|
||||||
`,
|
|
||||||
output: "6300000006565b01",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
ch := Lex([]byte(test.input), false)
|
|
||||||
c := NewCompiler(false)
|
|
||||||
c.Feed(ch)
|
|
||||||
output, err := c.Compile()
|
|
||||||
if len(err) != 0 {
|
|
||||||
t.Errorf("compile error: %v\ninput: %s", err, test.input)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if output != test.output {
|
|
||||||
t.Errorf("incorrect output\ninput: %sgot: %s\nwant: %s\n", test.input, output, test.output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func lexAll(src string) []token {
|
|
||||||
ch := Lex([]byte(src), false)
|
|
||||||
|
|
||||||
var tokens []token
|
|
||||||
for i := range ch {
|
|
||||||
tokens = append(tokens, i)
|
|
||||||
}
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLexer(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
tokens []token
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: ";; this is a comment",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "0x12345678",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: number, text: "0x12345678"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "0x123ggg",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: number, text: "0x123"}, {typ: element, text: "ggg"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "12345678",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: number, text: "12345678"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "123abc",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: number, text: "123"}, {typ: element, text: "abc"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "0123abc",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: number, text: "0123"}, {typ: element, text: "abc"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "00123abc",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: number, text: "00123"}, {typ: element, text: "abc"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "@foo",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: label, text: "foo"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "@label123",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: label, text: "label123"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
// Comment after label
|
|
||||||
{
|
|
||||||
input: "@label123 ;; comment",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: label, text: "label123"}, {typ: eof}},
|
|
||||||
},
|
|
||||||
// Comment after instruction
|
|
||||||
{
|
|
||||||
input: "push 3 ;; comment\nadd",
|
|
||||||
tokens: []token{{typ: lineStart}, {typ: element, text: "push"}, {typ: number, text: "3"}, {typ: lineEnd, text: "\n"}, {typ: lineStart, lineno: 1}, {typ: element, lineno: 1, text: "add"}, {typ: eof, lineno: 1}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
tokens := lexAll(test.input)
|
|
||||||
if !reflect.DeepEqual(tokens, test.tokens) {
|
|
||||||
t.Errorf("input %q\ngot: %+v\nwant: %+v", test.input, tokens, test.tokens)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// stateFn is used through the lifetime of the
|
|
||||||
// lexer to parse the different values at the
|
|
||||||
// current state.
|
|
||||||
type stateFn func(*lexer) stateFn
|
|
||||||
|
|
||||||
// token is emitted when the lexer has discovered
|
|
||||||
// a new parsable token. These are delivered over
|
|
||||||
// the tokens channels of the lexer
|
|
||||||
type token struct {
|
|
||||||
typ tokenType
|
|
||||||
lineno int
|
|
||||||
text string
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenType are the different types the lexer
|
|
||||||
// is able to parse and return.
|
|
||||||
type tokenType int
|
|
||||||
|
|
||||||
//go:generate go run golang.org/x/tools/cmd/stringer -type tokenType
|
|
||||||
|
|
||||||
const (
|
|
||||||
eof tokenType = iota // end of file
|
|
||||||
lineStart // emitted when a line starts
|
|
||||||
lineEnd // emitted when a line ends
|
|
||||||
invalidStatement // any invalid statement
|
|
||||||
element // any element during element parsing
|
|
||||||
label // label is emitted when a label is found
|
|
||||||
labelDef // label definition is emitted when a new label is found
|
|
||||||
number // number is emitted when a number is found
|
|
||||||
stringValue // stringValue is emitted when a string has been found
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
decimalNumbers = "1234567890" // characters representing any decimal number
|
|
||||||
hexNumbers = decimalNumbers + "aAbBcCdDeEfF" // characters representing any hexadecimal
|
|
||||||
alpha = "abcdefghijklmnopqrstuwvxyzABCDEFGHIJKLMNOPQRSTUWVXYZ" // characters representing alphanumeric
|
|
||||||
)
|
|
||||||
|
|
||||||
// lexer is the basic construct for parsing
|
|
||||||
// source code and turning them in to tokens.
|
|
||||||
// Tokens are interpreted by the compiler.
|
|
||||||
type lexer struct {
|
|
||||||
input string // input contains the source code of the program
|
|
||||||
|
|
||||||
tokens chan token // tokens is used to deliver tokens to the listener
|
|
||||||
state stateFn // the current state function
|
|
||||||
|
|
||||||
lineno int // current line number in the source file
|
|
||||||
start, pos, width int // positions for lexing and returning value
|
|
||||||
|
|
||||||
debug bool // flag for triggering debug output
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lex lexes the program by name with the given source. It returns a
|
|
||||||
// channel on which the tokens are delivered.
|
|
||||||
func Lex(source []byte, debug bool) <-chan token {
|
|
||||||
ch := make(chan token)
|
|
||||||
l := &lexer{
|
|
||||||
input: string(source),
|
|
||||||
tokens: ch,
|
|
||||||
state: lexLine,
|
|
||||||
debug: debug,
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
l.emit(lineStart)
|
|
||||||
for l.state != nil {
|
|
||||||
l.state = l.state(l)
|
|
||||||
}
|
|
||||||
l.emit(eof)
|
|
||||||
close(l.tokens)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns the next rune in the program's source.
|
|
||||||
func (l *lexer) next() (rune rune) {
|
|
||||||
if l.pos >= len(l.input) {
|
|
||||||
l.width = 0
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
rune, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
|
|
||||||
l.pos += l.width
|
|
||||||
return rune
|
|
||||||
}
|
|
||||||
|
|
||||||
// backup backsup the last parsed element (multi-character)
|
|
||||||
func (l *lexer) backup() {
|
|
||||||
l.pos -= l.width
|
|
||||||
}
|
|
||||||
|
|
||||||
// peek returns the next rune but does not advance the seeker
|
|
||||||
func (l *lexer) peek() rune {
|
|
||||||
r := l.next()
|
|
||||||
l.backup()
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore advances the seeker and ignores the value
|
|
||||||
func (l *lexer) ignore() {
|
|
||||||
l.start = l.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accepts checks whether the given input matches the next rune
|
|
||||||
func (l *lexer) accept(valid string) bool {
|
|
||||||
if strings.ContainsRune(valid, l.next()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
l.backup()
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// acceptRun will continue to advance the seeker until valid
|
|
||||||
// can no longer be met.
|
|
||||||
func (l *lexer) acceptRun(valid string) {
|
|
||||||
for strings.ContainsRune(valid, l.next()) {
|
|
||||||
}
|
|
||||||
l.backup()
|
|
||||||
}
|
|
||||||
|
|
||||||
// acceptRunUntil is the inverse of acceptRun and will continue
|
|
||||||
// to advance the seeker until the rune has been found.
|
|
||||||
func (l *lexer) acceptRunUntil(until rune) bool {
|
|
||||||
// Continues running until a rune is found
|
|
||||||
for i := l.next(); !strings.ContainsRune(string(until), i); i = l.next() {
|
|
||||||
if i == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// blob returns the current value
|
|
||||||
func (l *lexer) blob() string {
|
|
||||||
return l.input[l.start:l.pos]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emits a new token on to token channel for processing
|
|
||||||
func (l *lexer) emit(t tokenType) {
|
|
||||||
token := token{t, l.lineno, l.blob()}
|
|
||||||
|
|
||||||
if l.debug {
|
|
||||||
fmt.Fprintf(os.Stderr, "%04d: (%-20v) %s\n", token.lineno, token.typ, token.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.tokens <- token
|
|
||||||
l.start = l.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexLine is state function for lexing lines
|
|
||||||
func lexLine(l *lexer) stateFn {
|
|
||||||
for {
|
|
||||||
switch r := l.next(); {
|
|
||||||
case r == '\n':
|
|
||||||
l.emit(lineEnd)
|
|
||||||
l.ignore()
|
|
||||||
l.lineno++
|
|
||||||
l.emit(lineStart)
|
|
||||||
case r == ';' && l.peek() == ';':
|
|
||||||
return lexComment
|
|
||||||
case isSpace(r):
|
|
||||||
l.ignore()
|
|
||||||
case isLetter(r) || r == '_':
|
|
||||||
return lexElement
|
|
||||||
case isNumber(r):
|
|
||||||
return lexNumber
|
|
||||||
case r == '@':
|
|
||||||
l.ignore()
|
|
||||||
return lexLabel
|
|
||||||
case r == '"':
|
|
||||||
return lexInsideString
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexComment parses the current position until the end
|
|
||||||
// of the line and discards the text.
|
|
||||||
func lexComment(l *lexer) stateFn {
|
|
||||||
l.acceptRunUntil('\n')
|
|
||||||
l.backup()
|
|
||||||
l.ignore()
|
|
||||||
|
|
||||||
return lexLine
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexLabel parses the current label, emits and returns
|
|
||||||
// the lex text state function to advance the parsing
|
|
||||||
// process.
|
|
||||||
func lexLabel(l *lexer) stateFn {
|
|
||||||
l.acceptRun(alpha + "_" + decimalNumbers)
|
|
||||||
|
|
||||||
l.emit(label)
|
|
||||||
|
|
||||||
return lexLine
|
|
||||||
}
|
|
||||||
|
|
||||||
// lexInsideString lexes the inside of a string until
|
|
||||||
// the state function finds the closing quote.
|
|
||||||
// It returns the lex text state function.
|
|
||||||
func lexInsideString(l *lexer) stateFn {
|
|
||||||
if l.acceptRunUntil('"') {
|
|
||||||
l.emit(stringValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lexLine
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexNumber(l *lexer) stateFn {
|
|
||||||
acceptance := decimalNumbers
|
|
||||||
if l.accept("xX") {
|
|
||||||
acceptance = hexNumbers
|
|
||||||
}
|
|
||||||
l.acceptRun(acceptance)
|
|
||||||
|
|
||||||
l.emit(number)
|
|
||||||
|
|
||||||
return lexLine
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexElement(l *lexer) stateFn {
|
|
||||||
l.acceptRun(alpha + "_" + decimalNumbers)
|
|
||||||
|
|
||||||
if l.peek() == ':' {
|
|
||||||
l.emit(labelDef)
|
|
||||||
|
|
||||||
l.accept(":")
|
|
||||||
l.ignore()
|
|
||||||
} else {
|
|
||||||
l.emit(element)
|
|
||||||
}
|
|
||||||
return lexLine
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLetter(t rune) bool {
|
|
||||||
return unicode.IsLetter(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSpace(t rune) bool {
|
|
||||||
return unicode.IsSpace(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNumber(t rune) bool {
|
|
||||||
return unicode.IsNumber(t)
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
// Code generated by "stringer -type tokenType"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package asm
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
func _() {
|
|
||||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
|
||||||
// Re-run the stringer command to generate them again.
|
|
||||||
var x [1]struct{}
|
|
||||||
_ = x[eof-0]
|
|
||||||
_ = x[lineStart-1]
|
|
||||||
_ = x[lineEnd-2]
|
|
||||||
_ = x[invalidStatement-3]
|
|
||||||
_ = x[element-4]
|
|
||||||
_ = x[label-5]
|
|
||||||
_ = x[labelDef-6]
|
|
||||||
_ = x[number-7]
|
|
||||||
_ = x[stringValue-8]
|
|
||||||
}
|
|
||||||
|
|
||||||
const _tokenType_name = "eoflineStartlineEndinvalidStatementelementlabellabelDefnumberstringValue"
|
|
||||||
|
|
||||||
var _tokenType_index = [...]uint8{0, 3, 12, 19, 35, 42, 47, 55, 61, 72}
|
|
||||||
|
|
||||||
func (i tokenType) String() string {
|
|
||||||
if i < 0 || i >= tokenType(len(_tokenType_index)-1) {
|
|
||||||
return "tokenType(" + strconv.FormatInt(int64(i), 10) + ")"
|
|
||||||
}
|
|
||||||
return _tokenType_name[_tokenType_index[i]:_tokenType_index[i+1]]
|
|
||||||
}
|
|
||||||
|
|
@ -1,326 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkInsertChain_empty_memdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, false, nil)
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_empty_diskdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, true, nil)
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_valueTx_memdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, false, genValueTx(0))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_valueTx_diskdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, true, genValueTx(0))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_valueTx_100kB_memdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, false, genValueTx(100*1024))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_valueTx_100kB_diskdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, true, genValueTx(100*1024))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_uncles_memdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, false, genUncles)
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_uncles_diskdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, true, genUncles)
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_ring200_memdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, false, genTxRing(200))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_ring200_diskdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, true, genTxRing(200))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_ring1000_memdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, false, genTxRing(1000))
|
|
||||||
}
|
|
||||||
func BenchmarkInsertChain_ring1000_diskdb(b *testing.B) {
|
|
||||||
benchInsertChain(b, true, genTxRing(1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// This is the content of the genesis block used by the benchmarks.
|
|
||||||
benchRootKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
benchRootAddr = crypto.PubkeyToAddress(benchRootKey.PublicKey)
|
|
||||||
benchRootFunds = math.BigPow(2, 200)
|
|
||||||
)
|
|
||||||
|
|
||||||
// genValueTx returns a block generator that includes a single
|
|
||||||
// value-transfer transaction with n bytes of extra data in each
|
|
||||||
// block.
|
|
||||||
func genValueTx(nbytes int) func(int, *BlockGen) {
|
|
||||||
return func(i int, gen *BlockGen) {
|
|
||||||
toaddr := common.Address{}
|
|
||||||
data := make([]byte, nbytes)
|
|
||||||
gas, _ := IntrinsicGas(data, nil, false, false, false, false)
|
|
||||||
signer := gen.Signer()
|
|
||||||
gasPrice := big.NewInt(0)
|
|
||||||
if gen.header.BaseFee != nil {
|
|
||||||
gasPrice = gen.header.BaseFee
|
|
||||||
}
|
|
||||||
tx, _ := types.SignNewTx(benchRootKey, signer, &types.LegacyTx{
|
|
||||||
Nonce: gen.TxNonce(benchRootAddr),
|
|
||||||
To: &toaddr,
|
|
||||||
Value: big.NewInt(1),
|
|
||||||
Gas: gas,
|
|
||||||
Data: data,
|
|
||||||
GasPrice: gasPrice,
|
|
||||||
})
|
|
||||||
gen.AddTx(tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ringKeys = make([]*ecdsa.PrivateKey, 1000)
|
|
||||||
ringAddrs = make([]common.Address, len(ringKeys))
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
ringKeys[0] = benchRootKey
|
|
||||||
ringAddrs[0] = benchRootAddr
|
|
||||||
for i := 1; i < len(ringKeys); i++ {
|
|
||||||
ringKeys[i], _ = crypto.GenerateKey()
|
|
||||||
ringAddrs[i] = crypto.PubkeyToAddress(ringKeys[i].PublicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// genTxRing returns a block generator that sends ether in a ring
|
|
||||||
// among n accounts. This is creates n entries in the state database
|
|
||||||
// and fills the blocks with many small transactions.
|
|
||||||
func genTxRing(naccounts int) func(int, *BlockGen) {
|
|
||||||
from := 0
|
|
||||||
availableFunds := new(big.Int).Set(benchRootFunds)
|
|
||||||
return func(i int, gen *BlockGen) {
|
|
||||||
block := gen.PrevBlock(i - 1)
|
|
||||||
gas := block.GasLimit()
|
|
||||||
gasPrice := big.NewInt(0)
|
|
||||||
if gen.header.BaseFee != nil {
|
|
||||||
gasPrice = gen.header.BaseFee
|
|
||||||
}
|
|
||||||
signer := gen.Signer()
|
|
||||||
for {
|
|
||||||
gas -= params.TxGas
|
|
||||||
if gas < params.TxGas {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
to := (from + 1) % naccounts
|
|
||||||
burn := new(big.Int).SetUint64(params.TxGas)
|
|
||||||
burn.Mul(burn, gen.header.BaseFee)
|
|
||||||
availableFunds.Sub(availableFunds, burn)
|
|
||||||
if availableFunds.Cmp(big.NewInt(1)) < 0 {
|
|
||||||
panic("not enough funds")
|
|
||||||
}
|
|
||||||
tx, err := types.SignNewTx(ringKeys[from], signer,
|
|
||||||
&types.LegacyTx{
|
|
||||||
Nonce: gen.TxNonce(ringAddrs[from]),
|
|
||||||
To: &ringAddrs[to],
|
|
||||||
Value: availableFunds,
|
|
||||||
Gas: params.TxGas,
|
|
||||||
GasPrice: gasPrice,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
gen.AddTx(tx)
|
|
||||||
from = to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// genUncles generates blocks with two uncle headers.
|
|
||||||
func genUncles(i int, gen *BlockGen) {
|
|
||||||
if i >= 7 {
|
|
||||||
b2 := gen.PrevBlock(i - 6).Header()
|
|
||||||
b2.Extra = []byte("foo")
|
|
||||||
gen.AddUncle(b2)
|
|
||||||
b3 := gen.PrevBlock(i - 6).Header()
|
|
||||||
b3.Extra = []byte("bar")
|
|
||||||
gen.AddUncle(b3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func benchInsertChain(b *testing.B, disk bool, gen func(int, *BlockGen)) {
|
|
||||||
// Create the database in memory or in a temporary directory.
|
|
||||||
var db ethdb.Database
|
|
||||||
var err error
|
|
||||||
if !disk {
|
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
} else {
|
|
||||||
dir := b.TempDir()
|
|
||||||
db, err = rawdb.NewLevelDBDatabase(dir, 128, 128, "", false)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("cannot create temporary database: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a chain of b.N blocks using the supplied block
|
|
||||||
// generator function.
|
|
||||||
gspec := &Genesis{
|
|
||||||
Config: params.TestChainConfig,
|
|
||||||
Alloc: GenesisAlloc{benchRootAddr: {Balance: benchRootFunds}},
|
|
||||||
}
|
|
||||||
_, chain, _ := GenerateChainWithGenesis(gspec, ethash.NewFaker(), b.N, gen)
|
|
||||||
|
|
||||||
// Time the insertion of the new chain.
|
|
||||||
// State and blocks are stored in the same DB.
|
|
||||||
chainman, _ := NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer chainman.Stop()
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
if i, err := chainman.InsertChain(chain); err != nil {
|
|
||||||
b.Fatalf("insert error (block %d): %v\n", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkChainRead_header_10k(b *testing.B) {
|
|
||||||
benchReadChain(b, false, 10000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainRead_full_10k(b *testing.B) {
|
|
||||||
benchReadChain(b, true, 10000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainRead_header_100k(b *testing.B) {
|
|
||||||
benchReadChain(b, false, 100000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainRead_full_100k(b *testing.B) {
|
|
||||||
benchReadChain(b, true, 100000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainRead_header_500k(b *testing.B) {
|
|
||||||
benchReadChain(b, false, 500000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainRead_full_500k(b *testing.B) {
|
|
||||||
benchReadChain(b, true, 500000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainWrite_header_10k(b *testing.B) {
|
|
||||||
benchWriteChain(b, false, 10000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainWrite_full_10k(b *testing.B) {
|
|
||||||
benchWriteChain(b, true, 10000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainWrite_header_100k(b *testing.B) {
|
|
||||||
benchWriteChain(b, false, 100000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainWrite_full_100k(b *testing.B) {
|
|
||||||
benchWriteChain(b, true, 100000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainWrite_header_500k(b *testing.B) {
|
|
||||||
benchWriteChain(b, false, 500000)
|
|
||||||
}
|
|
||||||
func BenchmarkChainWrite_full_500k(b *testing.B) {
|
|
||||||
benchWriteChain(b, true, 500000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeChainForBench writes a given number of headers or empty blocks/receipts
|
|
||||||
// into a database.
|
|
||||||
func makeChainForBench(db ethdb.Database, full bool, count uint64) {
|
|
||||||
var hash common.Hash
|
|
||||||
for n := uint64(0); n < count; n++ {
|
|
||||||
header := &types.Header{
|
|
||||||
Coinbase: common.Address{},
|
|
||||||
Number: big.NewInt(int64(n)),
|
|
||||||
ParentHash: hash,
|
|
||||||
Difficulty: big.NewInt(1),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
}
|
|
||||||
hash = header.Hash()
|
|
||||||
|
|
||||||
rawdb.WriteHeader(db, header)
|
|
||||||
rawdb.WriteCanonicalHash(db, hash, n)
|
|
||||||
rawdb.WriteTd(db, hash, n, big.NewInt(int64(n+1)))
|
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
rawdb.WriteChainConfig(db, hash, params.AllEthashProtocolChanges)
|
|
||||||
}
|
|
||||||
rawdb.WriteHeadHeaderHash(db, hash)
|
|
||||||
|
|
||||||
if full || n == 0 {
|
|
||||||
block := types.NewBlockWithHeader(header)
|
|
||||||
rawdb.WriteBody(db, hash, n, block.Body())
|
|
||||||
rawdb.WriteReceipts(db, hash, n, nil)
|
|
||||||
rawdb.WriteHeadBlockHash(db, hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func benchWriteChain(b *testing.B, full bool, count uint64) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
dir := b.TempDir()
|
|
||||||
db, err := rawdb.NewLevelDBDatabase(dir, 128, 1024, "", false)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("error opening database at %v: %v", dir, err)
|
|
||||||
}
|
|
||||||
makeChainForBench(db, full, count)
|
|
||||||
db.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func benchReadChain(b *testing.B, full bool, count uint64) {
|
|
||||||
dir := b.TempDir()
|
|
||||||
|
|
||||||
db, err := rawdb.NewLevelDBDatabase(dir, 128, 1024, "", false)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("error opening database at %v: %v", dir, err)
|
|
||||||
}
|
|
||||||
makeChainForBench(db, full, count)
|
|
||||||
db.Close()
|
|
||||||
cacheConfig := *defaultCacheConfig
|
|
||||||
cacheConfig.TrieDirtyDisabled = true
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
db, err := rawdb.NewLevelDBDatabase(dir, 128, 1024, "", false)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("error opening database at %v: %v", dir, err)
|
|
||||||
}
|
|
||||||
chain, err := NewBlockChain(db, &cacheConfig, nil, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("error creating chain: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for n := uint64(0); n < count; n++ {
|
|
||||||
header := chain.GetHeaderByNumber(n)
|
|
||||||
if full {
|
|
||||||
hash := header.Hash()
|
|
||||||
rawdb.ReadBody(db, hash, n)
|
|
||||||
rawdb.ReadReceipts(db, hash, n, header.Time, chain.Config())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chain.Stop()
|
|
||||||
db.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlockValidator is responsible for validating block headers, uncles and
|
|
||||||
// processed state.
|
|
||||||
//
|
|
||||||
// BlockValidator implements Validator.
|
|
||||||
type BlockValidator struct {
|
|
||||||
config *params.ChainConfig // Chain configuration options
|
|
||||||
bc *BlockChain // Canonical block chain
|
|
||||||
engine consensus.Engine // Consensus engine used for validating
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBlockValidator returns a new block validator which is safe for re-use
|
|
||||||
func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engine consensus.Engine) *BlockValidator {
|
|
||||||
validator := &BlockValidator{
|
|
||||||
config: config,
|
|
||||||
engine: engine,
|
|
||||||
bc: blockchain,
|
|
||||||
}
|
|
||||||
return validator
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateBody validates the given block's uncles and verifies the block
|
|
||||||
// header's transaction and uncle roots. The headers are assumed to be already
|
|
||||||
// validated at this point.
|
|
||||||
func (v *BlockValidator) ValidateBody(block *types.Block) error {
|
|
||||||
// Check whether the block is already imported.
|
|
||||||
if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) {
|
|
||||||
return ErrKnownBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header validity is known at this point. Here we verify that uncles, transactions
|
|
||||||
// and withdrawals given in the block body match the header.
|
|
||||||
header := block.Header()
|
|
||||||
if err := v.engine.VerifyUncles(v.bc, block); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash {
|
|
||||||
return fmt.Errorf("uncle root hash mismatch (header value %x, calculated %x)", header.UncleHash, hash)
|
|
||||||
}
|
|
||||||
if hash := types.DeriveSha(block.Transactions(), trie.NewStackTrie(nil)); hash != header.TxHash {
|
|
||||||
return fmt.Errorf("transaction root hash mismatch (header value %x, calculated %x)", header.TxHash, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Withdrawals are present after the Shanghai fork.
|
|
||||||
if header.WithdrawalsHash != nil {
|
|
||||||
// Withdrawals list must be present in body after Shanghai.
|
|
||||||
if block.Withdrawals() == nil {
|
|
||||||
return errors.New("missing withdrawals in block body")
|
|
||||||
}
|
|
||||||
if hash := types.DeriveSha(block.Withdrawals(), trie.NewStackTrie(nil)); hash != *header.WithdrawalsHash {
|
|
||||||
return fmt.Errorf("withdrawals root hash mismatch (header value %x, calculated %x)", *header.WithdrawalsHash, hash)
|
|
||||||
}
|
|
||||||
} else if block.Withdrawals() != nil {
|
|
||||||
// Withdrawals are not allowed prior to Shanghai fork
|
|
||||||
return errors.New("withdrawals present in block body")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blob transactions may be present after the Cancun fork.
|
|
||||||
var blobs int
|
|
||||||
for i, tx := range block.Transactions() {
|
|
||||||
// Count the number of blobs to validate against the header's blobGasUsed
|
|
||||||
blobs += len(tx.BlobHashes())
|
|
||||||
|
|
||||||
// If the tx is a blob tx, it must NOT have a sidecar attached to be valid in a block.
|
|
||||||
if tx.BlobTxSidecar() != nil {
|
|
||||||
return fmt.Errorf("unexpected blob sidecar in transaction at index %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The individual checks for blob validity (version-check + not empty)
|
|
||||||
// happens in StateTransition.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check blob gas usage.
|
|
||||||
if header.BlobGasUsed != nil {
|
|
||||||
if want := *header.BlobGasUsed / params.BlobTxBlobGasPerBlob; uint64(blobs) != want { // div because the header is surely good vs the body might be bloated
|
|
||||||
return fmt.Errorf("blob gas used mismatch (header %v, calculated %v)", *header.BlobGasUsed, blobs*params.BlobTxBlobGasPerBlob)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if blobs > 0 {
|
|
||||||
return errors.New("data blobs present in block body")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancestor block must be known.
|
|
||||||
if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
|
|
||||||
if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) {
|
|
||||||
return consensus.ErrUnknownAncestor
|
|
||||||
}
|
|
||||||
return consensus.ErrPrunedAncestor
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateState validates the various changes that happen after a state transition,
|
|
||||||
// such as amount of used gas, the receipt roots and the state root itself.
|
|
||||||
func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64) error {
|
|
||||||
header := block.Header()
|
|
||||||
if block.GasUsed() != usedGas {
|
|
||||||
return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas)
|
|
||||||
}
|
|
||||||
// Validate the received block's bloom with the one derived from the generated receipts.
|
|
||||||
// For valid blocks this should always validate to true.
|
|
||||||
rbloom := types.CreateBloom(receipts)
|
|
||||||
if rbloom != header.Bloom {
|
|
||||||
return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom)
|
|
||||||
}
|
|
||||||
// Tre receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]]))
|
|
||||||
receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil))
|
|
||||||
if receiptSha != header.ReceiptHash {
|
|
||||||
return fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha)
|
|
||||||
}
|
|
||||||
// Validate the state root against the received state root and throw
|
|
||||||
// an error if they don't match.
|
|
||||||
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root {
|
|
||||||
return fmt.Errorf("invalid merkle root (remote: %x local: %x) dberr: %w", header.Root, root, statedb.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalcGasLimit computes the gas limit of the next block after parent. It aims
|
|
||||||
// to keep the baseline gas close to the provided target, and increase it towards
|
|
||||||
// the target if the baseline gas is lower.
|
|
||||||
func CalcGasLimit(parentGasLimit, desiredLimit uint64) uint64 {
|
|
||||||
delta := parentGasLimit/params.GasLimitBoundDivisor - 1
|
|
||||||
limit := parentGasLimit
|
|
||||||
if desiredLimit < params.MinGasLimit {
|
|
||||||
desiredLimit = params.MinGasLimit
|
|
||||||
}
|
|
||||||
// If we're outside our allowed gas range, we try to hone towards them
|
|
||||||
if limit < desiredLimit {
|
|
||||||
limit = parentGasLimit + delta
|
|
||||||
if limit > desiredLimit {
|
|
||||||
limit = desiredLimit
|
|
||||||
}
|
|
||||||
return limit
|
|
||||||
}
|
|
||||||
if limit > desiredLimit {
|
|
||||||
limit = parentGasLimit - delta
|
|
||||||
if limit < desiredLimit {
|
|
||||||
limit = desiredLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return limit
|
|
||||||
}
|
|
||||||
|
|
@ -1,272 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/beacon"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/clique"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that simple header verification works, for both good and bad blocks.
|
|
||||||
func TestHeaderVerification(t *testing.T) {
|
|
||||||
testHeaderVerification(t, rawdb.HashScheme)
|
|
||||||
testHeaderVerification(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testHeaderVerification(t *testing.T, scheme string) {
|
|
||||||
// Create a simple chain to verify
|
|
||||||
var (
|
|
||||||
gspec = &Genesis{Config: params.TestChainConfig}
|
|
||||||
_, blocks, _ = GenerateChainWithGenesis(gspec, ethash.NewFaker(), 8, nil)
|
|
||||||
)
|
|
||||||
headers := make([]*types.Header, len(blocks))
|
|
||||||
for i, block := range blocks {
|
|
||||||
headers[i] = block.Header()
|
|
||||||
}
|
|
||||||
// Run the header checker for blocks one-by-one, checking for both valid and invalid nonces
|
|
||||||
chain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer chain.Stop()
|
|
||||||
|
|
||||||
for i := 0; i < len(blocks); i++ {
|
|
||||||
for j, valid := range []bool{true, false} {
|
|
||||||
var results <-chan error
|
|
||||||
|
|
||||||
if valid {
|
|
||||||
engine := ethash.NewFaker()
|
|
||||||
_, results = engine.VerifyHeaders(chain, []*types.Header{headers[i]})
|
|
||||||
} else {
|
|
||||||
engine := ethash.NewFakeFailer(headers[i].Number.Uint64())
|
|
||||||
_, results = engine.VerifyHeaders(chain, []*types.Header{headers[i]})
|
|
||||||
}
|
|
||||||
// Wait for the verification result
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
if (result == nil) != valid {
|
|
||||||
t.Errorf("test %d.%d: validity mismatch: have %v, want %v", i, j, result, valid)
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("test %d.%d: verification timeout", i, j)
|
|
||||||
}
|
|
||||||
// Make sure no more data is returned
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
t.Fatalf("test %d.%d: unexpected result returned: %v", i, j, result)
|
|
||||||
case <-time.After(25 * time.Millisecond):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chain.InsertChain(blocks[i : i+1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeaderVerificationForMergingClique(t *testing.T) { testHeaderVerificationForMerging(t, true) }
|
|
||||||
func TestHeaderVerificationForMergingEthash(t *testing.T) { testHeaderVerificationForMerging(t, false) }
|
|
||||||
|
|
||||||
// Tests the verification for eth1/2 merging, including pre-merge and post-merge
|
|
||||||
func testHeaderVerificationForMerging(t *testing.T, isClique bool) {
|
|
||||||
var (
|
|
||||||
gspec *Genesis
|
|
||||||
preBlocks []*types.Block
|
|
||||||
postBlocks []*types.Block
|
|
||||||
engine consensus.Engine
|
|
||||||
merger = consensus.NewMerger(rawdb.NewMemoryDatabase())
|
|
||||||
)
|
|
||||||
if isClique {
|
|
||||||
var (
|
|
||||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
addr = crypto.PubkeyToAddress(key.PublicKey)
|
|
||||||
config = *params.AllCliqueProtocolChanges
|
|
||||||
)
|
|
||||||
engine = beacon.New(clique.New(params.AllCliqueProtocolChanges.Clique, rawdb.NewMemoryDatabase()))
|
|
||||||
gspec = &Genesis{
|
|
||||||
Config: &config,
|
|
||||||
ExtraData: make([]byte, 32+common.AddressLength+crypto.SignatureLength),
|
|
||||||
Alloc: map[common.Address]GenesisAccount{
|
|
||||||
addr: {Balance: big.NewInt(1)},
|
|
||||||
},
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Difficulty: new(big.Int),
|
|
||||||
}
|
|
||||||
copy(gspec.ExtraData[32:], addr[:])
|
|
||||||
|
|
||||||
td := 0
|
|
||||||
genDb, blocks, _ := GenerateChainWithGenesis(gspec, engine, 8, nil)
|
|
||||||
for i, block := range blocks {
|
|
||||||
header := block.Header()
|
|
||||||
if i > 0 {
|
|
||||||
header.ParentHash = blocks[i-1].Hash()
|
|
||||||
}
|
|
||||||
header.Extra = make([]byte, 32+crypto.SignatureLength)
|
|
||||||
header.Difficulty = big.NewInt(2)
|
|
||||||
|
|
||||||
sig, _ := crypto.Sign(engine.SealHash(header).Bytes(), key)
|
|
||||||
copy(header.Extra[len(header.Extra)-crypto.SignatureLength:], sig)
|
|
||||||
blocks[i] = block.WithSeal(header)
|
|
||||||
|
|
||||||
// calculate td
|
|
||||||
td += int(block.Difficulty().Uint64())
|
|
||||||
}
|
|
||||||
preBlocks = blocks
|
|
||||||
gspec.Config.TerminalTotalDifficulty = big.NewInt(int64(td))
|
|
||||||
postBlocks, _ = GenerateChain(gspec.Config, preBlocks[len(preBlocks)-1], engine, genDb, 8, nil)
|
|
||||||
} else {
|
|
||||||
config := *params.TestChainConfig
|
|
||||||
gspec = &Genesis{Config: &config}
|
|
||||||
engine = beacon.New(ethash.NewFaker())
|
|
||||||
td := int(params.GenesisDifficulty.Uint64())
|
|
||||||
genDb, blocks, _ := GenerateChainWithGenesis(gspec, engine, 8, nil)
|
|
||||||
for _, block := range blocks {
|
|
||||||
// calculate td
|
|
||||||
td += int(block.Difficulty().Uint64())
|
|
||||||
}
|
|
||||||
preBlocks = blocks
|
|
||||||
gspec.Config.TerminalTotalDifficulty = big.NewInt(int64(td))
|
|
||||||
t.Logf("Set ttd to %v\n", gspec.Config.TerminalTotalDifficulty)
|
|
||||||
postBlocks, _ = GenerateChain(gspec.Config, preBlocks[len(preBlocks)-1], engine, genDb, 8, func(i int, gen *BlockGen) {
|
|
||||||
gen.SetPoS()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Assemble header batch
|
|
||||||
preHeaders := make([]*types.Header, len(preBlocks))
|
|
||||||
for i, block := range preBlocks {
|
|
||||||
preHeaders[i] = block.Header()
|
|
||||||
t.Logf("Pre-merge header: %d", block.NumberU64())
|
|
||||||
}
|
|
||||||
postHeaders := make([]*types.Header, len(postBlocks))
|
|
||||||
for i, block := range postBlocks {
|
|
||||||
postHeaders[i] = block.Header()
|
|
||||||
t.Logf("Post-merge header: %d", block.NumberU64())
|
|
||||||
}
|
|
||||||
// Run the header checker for blocks one-by-one, checking for both valid and invalid nonces
|
|
||||||
chain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{}, nil, nil)
|
|
||||||
defer chain.Stop()
|
|
||||||
|
|
||||||
// Verify the blocks before the merging
|
|
||||||
for i := 0; i < len(preBlocks); i++ {
|
|
||||||
_, results := engine.VerifyHeaders(chain, []*types.Header{preHeaders[i]})
|
|
||||||
// Wait for the verification result
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
if result != nil {
|
|
||||||
t.Errorf("pre-block %d: verification failed %v", i, result)
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("pre-block %d: verification timeout", i)
|
|
||||||
}
|
|
||||||
// Make sure no more data is returned
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
t.Fatalf("pre-block %d: unexpected result returned: %v", i, result)
|
|
||||||
case <-time.After(25 * time.Millisecond):
|
|
||||||
}
|
|
||||||
chain.InsertChain(preBlocks[i : i+1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the transition
|
|
||||||
merger.ReachTTD()
|
|
||||||
merger.FinalizePoS()
|
|
||||||
|
|
||||||
// Verify the blocks after the merging
|
|
||||||
for i := 0; i < len(postBlocks); i++ {
|
|
||||||
_, results := engine.VerifyHeaders(chain, []*types.Header{postHeaders[i]})
|
|
||||||
// Wait for the verification result
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
if result != nil {
|
|
||||||
t.Errorf("post-block %d: verification failed %v", i, result)
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("test %d: verification timeout", i)
|
|
||||||
}
|
|
||||||
// Make sure no more data is returned
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
t.Fatalf("post-block %d: unexpected result returned: %v", i, result)
|
|
||||||
case <-time.After(25 * time.Millisecond):
|
|
||||||
}
|
|
||||||
chain.InsertBlockWithoutSetHead(postBlocks[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the blocks with pre-merge blocks and post-merge blocks
|
|
||||||
var headers []*types.Header
|
|
||||||
for _, block := range preBlocks {
|
|
||||||
headers = append(headers, block.Header())
|
|
||||||
}
|
|
||||||
for _, block := range postBlocks {
|
|
||||||
headers = append(headers, block.Header())
|
|
||||||
}
|
|
||||||
_, results := engine.VerifyHeaders(chain, headers)
|
|
||||||
for i := 0; i < len(headers); i++ {
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
if result != nil {
|
|
||||||
t.Errorf("test %d: verification failed %v", i, result)
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("test %d: verification timeout", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Make sure no more data is returned
|
|
||||||
select {
|
|
||||||
case result := <-results:
|
|
||||||
t.Fatalf("unexpected result returned: %v", result)
|
|
||||||
case <-time.After(25 * time.Millisecond):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalcGasLimit(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
pGasLimit uint64
|
|
||||||
max uint64
|
|
||||||
min uint64
|
|
||||||
}{
|
|
||||||
{20000000, 20019530, 19980470},
|
|
||||||
{40000000, 40039061, 39960939},
|
|
||||||
} {
|
|
||||||
// Increase
|
|
||||||
if have, want := CalcGasLimit(tc.pGasLimit, 2*tc.pGasLimit), tc.max; have != want {
|
|
||||||
t.Errorf("test %d: have %d want <%d", i, have, want)
|
|
||||||
}
|
|
||||||
// Decrease
|
|
||||||
if have, want := CalcGasLimit(tc.pGasLimit, 0), tc.min; have != want {
|
|
||||||
t.Errorf("test %d: have %d want >%d", i, have, want)
|
|
||||||
}
|
|
||||||
// Small decrease
|
|
||||||
if have, want := CalcGasLimit(tc.pGasLimit, tc.pGasLimit-1), tc.pGasLimit-1; have != want {
|
|
||||||
t.Errorf("test %d: have %d want %d", i, have, want)
|
|
||||||
}
|
|
||||||
// Small increase
|
|
||||||
if have, want := CalcGasLimit(tc.pGasLimit, tc.pGasLimit+1), tc.pGasLimit+1; have != want {
|
|
||||||
t.Errorf("test %d: have %d want %d", i, have, want)
|
|
||||||
}
|
|
||||||
// No change
|
|
||||||
if have, want := CalcGasLimit(tc.pGasLimit, tc.pGasLimit), tc.pGasLimit; have != want {
|
|
||||||
t.Errorf("test %d: have %d want %d", i, have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2591
core/blockchain.go
2591
core/blockchain.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,186 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// insertStats tracks and reports on block insertion.
|
|
||||||
type insertStats struct {
|
|
||||||
queued, processed, ignored int
|
|
||||||
usedGas uint64
|
|
||||||
lastIndex int
|
|
||||||
startTime mclock.AbsTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// statsReportLimit is the time limit during import and export after which we
|
|
||||||
// always print out progress. This avoids the user wondering what's going on.
|
|
||||||
const statsReportLimit = 8 * time.Second
|
|
||||||
|
|
||||||
// report prints statistics if some number of blocks have been processed
|
|
||||||
// or more than a few seconds have passed since the last message.
|
|
||||||
func (st *insertStats) report(chain []*types.Block, index int, snapDiffItems, snapBufItems, trieDiffNodes, triebufNodes common.StorageSize, setHead bool) {
|
|
||||||
// Fetch the timings for the batch
|
|
||||||
var (
|
|
||||||
now = mclock.Now()
|
|
||||||
elapsed = now.Sub(st.startTime)
|
|
||||||
)
|
|
||||||
// If we're at the last block of the batch or report period reached, log
|
|
||||||
if index == len(chain)-1 || elapsed >= statsReportLimit {
|
|
||||||
// Count the number of transactions in this segment
|
|
||||||
var txs int
|
|
||||||
for _, block := range chain[st.lastIndex : index+1] {
|
|
||||||
txs += len(block.Transactions())
|
|
||||||
}
|
|
||||||
end := chain[index]
|
|
||||||
|
|
||||||
// Assemble the log context and send it to the logger
|
|
||||||
context := []interface{}{
|
|
||||||
"number", end.Number(), "hash", end.Hash(),
|
|
||||||
"blocks", st.processed, "txs", txs, "mgas", float64(st.usedGas) / 1000000,
|
|
||||||
"elapsed", common.PrettyDuration(elapsed), "mgasps", float64(st.usedGas) * 1000 / float64(elapsed),
|
|
||||||
}
|
|
||||||
if timestamp := time.Unix(int64(end.Time()), 0); time.Since(timestamp) > time.Minute {
|
|
||||||
context = append(context, []interface{}{"age", common.PrettyAge(timestamp)}...)
|
|
||||||
}
|
|
||||||
if snapDiffItems != 0 || snapBufItems != 0 { // snapshots enabled
|
|
||||||
context = append(context, []interface{}{"snapdiffs", snapDiffItems}...)
|
|
||||||
if snapBufItems != 0 { // future snapshot refactor
|
|
||||||
context = append(context, []interface{}{"snapdirty", snapBufItems}...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if trieDiffNodes != 0 { // pathdb
|
|
||||||
context = append(context, []interface{}{"triediffs", trieDiffNodes}...)
|
|
||||||
}
|
|
||||||
context = append(context, []interface{}{"triedirty", triebufNodes}...)
|
|
||||||
|
|
||||||
if st.queued > 0 {
|
|
||||||
context = append(context, []interface{}{"queued", st.queued}...)
|
|
||||||
}
|
|
||||||
if st.ignored > 0 {
|
|
||||||
context = append(context, []interface{}{"ignored", st.ignored}...)
|
|
||||||
}
|
|
||||||
if setHead {
|
|
||||||
log.Info("Imported new chain segment", context...)
|
|
||||||
} else {
|
|
||||||
log.Info("Imported new potential chain segment", context...)
|
|
||||||
}
|
|
||||||
// Bump the stats reported to the next section
|
|
||||||
*st = insertStats{startTime: now, lastIndex: index + 1}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertIterator is a helper to assist during chain import.
|
|
||||||
type insertIterator struct {
|
|
||||||
chain types.Blocks // Chain of blocks being iterated over
|
|
||||||
|
|
||||||
results <-chan error // Verification result sink from the consensus engine
|
|
||||||
errors []error // Header verification errors for the blocks
|
|
||||||
|
|
||||||
index int // Current offset of the iterator
|
|
||||||
validator Validator // Validator to run if verification succeeds
|
|
||||||
}
|
|
||||||
|
|
||||||
// newInsertIterator creates a new iterator based on the given blocks, which are
|
|
||||||
// assumed to be a contiguous chain.
|
|
||||||
func newInsertIterator(chain types.Blocks, results <-chan error, validator Validator) *insertIterator {
|
|
||||||
return &insertIterator{
|
|
||||||
chain: chain,
|
|
||||||
results: results,
|
|
||||||
errors: make([]error, 0, len(chain)),
|
|
||||||
index: -1,
|
|
||||||
validator: validator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// next returns the next block in the iterator, along with any potential validation
|
|
||||||
// error for that block. When the end is reached, it will return (nil, nil).
|
|
||||||
func (it *insertIterator) next() (*types.Block, error) {
|
|
||||||
// If we reached the end of the chain, abort
|
|
||||||
if it.index+1 >= len(it.chain) {
|
|
||||||
it.index = len(it.chain)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
// Advance the iterator and wait for verification result if not yet done
|
|
||||||
it.index++
|
|
||||||
if len(it.errors) <= it.index {
|
|
||||||
it.errors = append(it.errors, <-it.results)
|
|
||||||
}
|
|
||||||
if it.errors[it.index] != nil {
|
|
||||||
return it.chain[it.index], it.errors[it.index]
|
|
||||||
}
|
|
||||||
// Block header valid, run body validation and return
|
|
||||||
return it.chain[it.index], it.validator.ValidateBody(it.chain[it.index])
|
|
||||||
}
|
|
||||||
|
|
||||||
// peek returns the next block in the iterator, along with any potential validation
|
|
||||||
// error for that block, but does **not** advance the iterator.
|
|
||||||
//
|
|
||||||
// Both header and body validation errors (nil too) is cached into the iterator
|
|
||||||
// to avoid duplicating work on the following next() call.
|
|
||||||
func (it *insertIterator) peek() (*types.Block, error) {
|
|
||||||
// If we reached the end of the chain, abort
|
|
||||||
if it.index+1 >= len(it.chain) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
// Wait for verification result if not yet done
|
|
||||||
if len(it.errors) <= it.index+1 {
|
|
||||||
it.errors = append(it.errors, <-it.results)
|
|
||||||
}
|
|
||||||
if it.errors[it.index+1] != nil {
|
|
||||||
return it.chain[it.index+1], it.errors[it.index+1]
|
|
||||||
}
|
|
||||||
// Block header valid, ignore body validation since we don't have a parent anyway
|
|
||||||
return it.chain[it.index+1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// previous returns the previous header that was being processed, or nil.
|
|
||||||
func (it *insertIterator) previous() *types.Header {
|
|
||||||
if it.index < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return it.chain[it.index-1].Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
// current returns the current header that is being processed, or nil.
|
|
||||||
func (it *insertIterator) current() *types.Header {
|
|
||||||
if it.index == -1 || it.index >= len(it.chain) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return it.chain[it.index].Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
// first returns the first block in the it.
|
|
||||||
func (it *insertIterator) first() *types.Block {
|
|
||||||
return it.chain[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// remaining returns the number of remaining blocks.
|
|
||||||
func (it *insertIterator) remaining() int {
|
|
||||||
return len(it.chain) - it.index
|
|
||||||
}
|
|
||||||
|
|
||||||
// processed returns the number of processed blocks.
|
|
||||||
func (it *insertIterator) processed() int {
|
|
||||||
return it.index + 1
|
|
||||||
}
|
|
||||||
|
|
@ -1,419 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/state"
|
|
||||||
"github.com/ethereum/go-ethereum/core/state/snapshot"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CurrentHeader retrieves the current head header of the canonical chain. The
|
|
||||||
// header is retrieved from the HeaderChain's internal cache.
|
|
||||||
func (bc *BlockChain) CurrentHeader() *types.Header {
|
|
||||||
return bc.hc.CurrentHeader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentBlock retrieves the current head block of the canonical chain. The
|
|
||||||
// block is retrieved from the blockchain's internal cache.
|
|
||||||
func (bc *BlockChain) CurrentBlock() *types.Header {
|
|
||||||
return bc.currentBlock.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentSnapBlock retrieves the current snap-sync head block of the canonical
|
|
||||||
// chain. The block is retrieved from the blockchain's internal cache.
|
|
||||||
func (bc *BlockChain) CurrentSnapBlock() *types.Header {
|
|
||||||
return bc.currentSnapBlock.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentFinalBlock retrieves the current finalized block of the canonical
|
|
||||||
// chain. The block is retrieved from the blockchain's internal cache.
|
|
||||||
func (bc *BlockChain) CurrentFinalBlock() *types.Header {
|
|
||||||
return bc.currentFinalBlock.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentSafeBlock retrieves the current safe block of the canonical
|
|
||||||
// chain. The block is retrieved from the blockchain's internal cache.
|
|
||||||
func (bc *BlockChain) CurrentSafeBlock() *types.Header {
|
|
||||||
return bc.currentSafeBlock.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasHeader checks if a block header is present in the database or not, caching
|
|
||||||
// it if present.
|
|
||||||
func (bc *BlockChain) HasHeader(hash common.Hash, number uint64) bool {
|
|
||||||
return bc.hc.HasHeader(hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeader retrieves a block header from the database by hash and number,
|
|
||||||
// caching it if found.
|
|
||||||
func (bc *BlockChain) GetHeader(hash common.Hash, number uint64) *types.Header {
|
|
||||||
return bc.hc.GetHeader(hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeaderByHash retrieves a block header from the database by hash, caching it if
|
|
||||||
// found.
|
|
||||||
func (bc *BlockChain) GetHeaderByHash(hash common.Hash) *types.Header {
|
|
||||||
return bc.hc.GetHeaderByHash(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeaderByNumber retrieves a block header from the database by number,
|
|
||||||
// caching it (associated with its hash) if found.
|
|
||||||
func (bc *BlockChain) GetHeaderByNumber(number uint64) *types.Header {
|
|
||||||
return bc.hc.GetHeaderByNumber(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeadersFrom returns a contiguous segment of headers, in rlp-form, going
|
|
||||||
// backwards from the given number.
|
|
||||||
func (bc *BlockChain) GetHeadersFrom(number, count uint64) []rlp.RawValue {
|
|
||||||
return bc.hc.GetHeadersFrom(number, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBody retrieves a block body (transactions and uncles) from the database by
|
|
||||||
// hash, caching it if found.
|
|
||||||
func (bc *BlockChain) GetBody(hash common.Hash) *types.Body {
|
|
||||||
// Short circuit if the body's already in the cache, retrieve otherwise
|
|
||||||
if cached, ok := bc.bodyCache.Get(hash); ok {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
number := bc.hc.GetBlockNumber(hash)
|
|
||||||
if number == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body := rawdb.ReadBody(bc.db, hash, *number)
|
|
||||||
if body == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Cache the found body for next time and return
|
|
||||||
bc.bodyCache.Add(hash, body)
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBodyRLP retrieves a block body in RLP encoding from the database by hash,
|
|
||||||
// caching it if found.
|
|
||||||
func (bc *BlockChain) GetBodyRLP(hash common.Hash) rlp.RawValue {
|
|
||||||
// Short circuit if the body's already in the cache, retrieve otherwise
|
|
||||||
if cached, ok := bc.bodyRLPCache.Get(hash); ok {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
number := bc.hc.GetBlockNumber(hash)
|
|
||||||
if number == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body := rawdb.ReadBodyRLP(bc.db, hash, *number)
|
|
||||||
if len(body) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Cache the found body for next time and return
|
|
||||||
bc.bodyRLPCache.Add(hash, body)
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasBlock checks if a block is fully present in the database or not.
|
|
||||||
func (bc *BlockChain) HasBlock(hash common.Hash, number uint64) bool {
|
|
||||||
if bc.blockCache.Contains(hash) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if !bc.HasHeader(hash, number) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return rawdb.HasBody(bc.db, hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasFastBlock checks if a fast block is fully present in the database or not.
|
|
||||||
func (bc *BlockChain) HasFastBlock(hash common.Hash, number uint64) bool {
|
|
||||||
if !bc.HasBlock(hash, number) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if bc.receiptsCache.Contains(hash) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return rawdb.HasReceipts(bc.db, hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlock retrieves a block from the database by hash and number,
|
|
||||||
// caching it if found.
|
|
||||||
func (bc *BlockChain) GetBlock(hash common.Hash, number uint64) *types.Block {
|
|
||||||
// Short circuit if the block's already in the cache, retrieve otherwise
|
|
||||||
if block, ok := bc.blockCache.Get(hash); ok {
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
block := rawdb.ReadBlock(bc.db, hash, number)
|
|
||||||
if block == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Cache the found block for next time and return
|
|
||||||
bc.blockCache.Add(block.Hash(), block)
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockByHash retrieves a block from the database by hash, caching it if found.
|
|
||||||
func (bc *BlockChain) GetBlockByHash(hash common.Hash) *types.Block {
|
|
||||||
number := bc.hc.GetBlockNumber(hash)
|
|
||||||
if number == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return bc.GetBlock(hash, *number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockByNumber retrieves a block from the database by number, caching it
|
|
||||||
// (associated with its hash) if found.
|
|
||||||
func (bc *BlockChain) GetBlockByNumber(number uint64) *types.Block {
|
|
||||||
hash := rawdb.ReadCanonicalHash(bc.db, number)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return bc.GetBlock(hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlocksFromHash returns the block corresponding to hash and up to n-1 ancestors.
|
|
||||||
// [deprecated by eth/62]
|
|
||||||
func (bc *BlockChain) GetBlocksFromHash(hash common.Hash, n int) (blocks []*types.Block) {
|
|
||||||
number := bc.hc.GetBlockNumber(hash)
|
|
||||||
if number == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
block := bc.GetBlock(hash, *number)
|
|
||||||
if block == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
blocks = append(blocks, block)
|
|
||||||
hash = block.ParentHash()
|
|
||||||
*number--
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetReceiptsByHash retrieves the receipts for all transactions in a given block.
|
|
||||||
func (bc *BlockChain) GetReceiptsByHash(hash common.Hash) types.Receipts {
|
|
||||||
if receipts, ok := bc.receiptsCache.Get(hash); ok {
|
|
||||||
return receipts
|
|
||||||
}
|
|
||||||
number := rawdb.ReadHeaderNumber(bc.db, hash)
|
|
||||||
if number == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
header := bc.GetHeader(hash, *number)
|
|
||||||
if header == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
receipts := rawdb.ReadReceipts(bc.db, hash, *number, header.Time, bc.chainConfig)
|
|
||||||
if receipts == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
bc.receiptsCache.Add(hash, receipts)
|
|
||||||
return receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUnclesInChain retrieves all the uncles from a given block backwards until
|
|
||||||
// a specific distance is reached.
|
|
||||||
func (bc *BlockChain) GetUnclesInChain(block *types.Block, length int) []*types.Header {
|
|
||||||
uncles := []*types.Header{}
|
|
||||||
for i := 0; block != nil && i < length; i++ {
|
|
||||||
uncles = append(uncles, block.Uncles()...)
|
|
||||||
block = bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
|
||||||
}
|
|
||||||
return uncles
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCanonicalHash returns the canonical hash for a given block number
|
|
||||||
func (bc *BlockChain) GetCanonicalHash(number uint64) common.Hash {
|
|
||||||
return bc.hc.GetCanonicalHash(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAncestor retrieves the Nth ancestor of a given block. It assumes that either the given block or
|
|
||||||
// a close ancestor of it is canonical. maxNonCanonical points to a downwards counter limiting the
|
|
||||||
// number of blocks to be individually checked before we reach the canonical chain.
|
|
||||||
//
|
|
||||||
// Note: ancestor == 0 returns the same block, 1 returns its parent and so on.
|
|
||||||
func (bc *BlockChain) GetAncestor(hash common.Hash, number, ancestor uint64, maxNonCanonical *uint64) (common.Hash, uint64) {
|
|
||||||
return bc.hc.GetAncestor(hash, number, ancestor, maxNonCanonical)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionLookup retrieves the lookup associate with the given transaction
|
|
||||||
// hash from the cache or database.
|
|
||||||
func (bc *BlockChain) GetTransactionLookup(hash common.Hash) *rawdb.LegacyTxLookupEntry {
|
|
||||||
// Short circuit if the txlookup already in the cache, retrieve otherwise
|
|
||||||
if lookup, exist := bc.txLookupCache.Get(hash); exist {
|
|
||||||
return lookup
|
|
||||||
}
|
|
||||||
tx, blockHash, blockNumber, txIndex := rawdb.ReadTransaction(bc.db, hash)
|
|
||||||
if tx == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lookup := &rawdb.LegacyTxLookupEntry{BlockHash: blockHash, BlockIndex: blockNumber, Index: txIndex}
|
|
||||||
bc.txLookupCache.Add(hash, lookup)
|
|
||||||
return lookup
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTd retrieves a block's total difficulty in the canonical chain from the
|
|
||||||
// database by hash and number, caching it if found.
|
|
||||||
func (bc *BlockChain) GetTd(hash common.Hash, number uint64) *big.Int {
|
|
||||||
return bc.hc.GetTd(hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasState checks if state trie is fully present in the database or not.
|
|
||||||
func (bc *BlockChain) HasState(hash common.Hash) bool {
|
|
||||||
_, err := bc.stateCache.OpenTrie(hash)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasBlockAndState checks if a block and associated state trie is fully present
|
|
||||||
// in the database or not, caching it if present.
|
|
||||||
func (bc *BlockChain) HasBlockAndState(hash common.Hash, number uint64) bool {
|
|
||||||
// Check first that the block itself is known
|
|
||||||
block := bc.GetBlock(hash, number)
|
|
||||||
if block == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bc.HasState(block.Root())
|
|
||||||
}
|
|
||||||
|
|
||||||
// stateRecoverable checks if the specified state is recoverable.
|
|
||||||
// Note, this function assumes the state is not present, because
|
|
||||||
// state is not treated as recoverable if it's available, thus
|
|
||||||
// false will be returned in this case.
|
|
||||||
func (bc *BlockChain) stateRecoverable(root common.Hash) bool {
|
|
||||||
if bc.triedb.Scheme() == rawdb.HashScheme {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
result, _ := bc.triedb.Recoverable(root)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContractCodeWithPrefix retrieves a blob of data associated with a contract
|
|
||||||
// hash either from ephemeral in-memory cache, or from persistent storage.
|
|
||||||
//
|
|
||||||
// If the code doesn't exist in the in-memory cache, check the storage with
|
|
||||||
// new code scheme.
|
|
||||||
func (bc *BlockChain) ContractCodeWithPrefix(hash common.Hash) ([]byte, error) {
|
|
||||||
type codeReader interface {
|
|
||||||
ContractCodeWithPrefix(address common.Address, codeHash common.Hash) ([]byte, error)
|
|
||||||
}
|
|
||||||
// TODO(rjl493456442) The associated account address is also required
|
|
||||||
// in Verkle scheme. Fix it once snap-sync is supported for Verkle.
|
|
||||||
return bc.stateCache.(codeReader).ContractCodeWithPrefix(common.Address{}, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// State returns a new mutable state based on the current HEAD block.
|
|
||||||
func (bc *BlockChain) State() (*state.StateDB, error) {
|
|
||||||
return bc.StateAt(bc.CurrentBlock().Root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateAt returns a new mutable state based on a particular point in time.
|
|
||||||
func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
|
|
||||||
return state.New(root, bc.stateCache, bc.snaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config retrieves the chain's fork configuration.
|
|
||||||
func (bc *BlockChain) Config() *params.ChainConfig { return bc.chainConfig }
|
|
||||||
|
|
||||||
// Engine retrieves the blockchain's consensus engine.
|
|
||||||
func (bc *BlockChain) Engine() consensus.Engine { return bc.engine }
|
|
||||||
|
|
||||||
// Snapshots returns the blockchain snapshot tree.
|
|
||||||
func (bc *BlockChain) Snapshots() *snapshot.Tree {
|
|
||||||
return bc.snaps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator returns the current validator.
|
|
||||||
func (bc *BlockChain) Validator() Validator {
|
|
||||||
return bc.validator
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processor returns the current processor.
|
|
||||||
func (bc *BlockChain) Processor() Processor {
|
|
||||||
return bc.processor
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateCache returns the caching database underpinning the blockchain instance.
|
|
||||||
func (bc *BlockChain) StateCache() state.Database {
|
|
||||||
return bc.stateCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// GasLimit returns the gas limit of the current HEAD block.
|
|
||||||
func (bc *BlockChain) GasLimit() uint64 {
|
|
||||||
return bc.CurrentBlock().GasLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
// Genesis retrieves the chain's genesis block.
|
|
||||||
func (bc *BlockChain) Genesis() *types.Block {
|
|
||||||
return bc.genesisBlock
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVMConfig returns the block chain VM config.
|
|
||||||
func (bc *BlockChain) GetVMConfig() *vm.Config {
|
|
||||||
return &bc.vmConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTxLookupLimit is responsible for updating the txlookup limit to the
|
|
||||||
// original one stored in db if the new mismatches with the old one.
|
|
||||||
func (bc *BlockChain) SetTxLookupLimit(limit uint64) {
|
|
||||||
bc.txLookupLimit = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxLookupLimit retrieves the txlookup limit used by blockchain to prune
|
|
||||||
// stale transaction indices.
|
|
||||||
func (bc *BlockChain) TxLookupLimit() uint64 {
|
|
||||||
return bc.txLookupLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrieDB retrieves the low level trie database used for data storage.
|
|
||||||
func (bc *BlockChain) TrieDB() *trie.Database {
|
|
||||||
return bc.triedb
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeRemovedLogsEvent registers a subscription of RemovedLogsEvent.
|
|
||||||
func (bc *BlockChain) SubscribeRemovedLogsEvent(ch chan<- RemovedLogsEvent) event.Subscription {
|
|
||||||
return bc.scope.Track(bc.rmLogsFeed.Subscribe(ch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeChainEvent registers a subscription of ChainEvent.
|
|
||||||
func (bc *BlockChain) SubscribeChainEvent(ch chan<- ChainEvent) event.Subscription {
|
|
||||||
return bc.scope.Track(bc.chainFeed.Subscribe(ch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeChainHeadEvent registers a subscription of ChainHeadEvent.
|
|
||||||
func (bc *BlockChain) SubscribeChainHeadEvent(ch chan<- ChainHeadEvent) event.Subscription {
|
|
||||||
return bc.scope.Track(bc.chainHeadFeed.Subscribe(ch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeChainSideEvent registers a subscription of ChainSideEvent.
|
|
||||||
func (bc *BlockChain) SubscribeChainSideEvent(ch chan<- ChainSideEvent) event.Subscription {
|
|
||||||
return bc.scope.Track(bc.chainSideFeed.Subscribe(ch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeLogsEvent registers a subscription of []*types.Log.
|
|
||||||
func (bc *BlockChain) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription {
|
|
||||||
return bc.scope.Track(bc.logsFeed.Subscribe(ch))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeBlockProcessingEvent registers a subscription of bool where true means
|
|
||||||
// block processing has started while false means it has stopped.
|
|
||||||
func (bc *BlockChain) SubscribeBlockProcessingEvent(ch chan<- bool) event.Subscription {
|
|
||||||
return bc.scope.Track(bc.blockProcFeed.Subscribe(ch))
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,713 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Tests that abnormal program termination (i.e.crash) and restart can recovery
|
|
||||||
// the snapshot properly if the snapshot is enabled.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
// snapshotTestBasic wraps the common testing fields in the snapshot tests.
|
|
||||||
type snapshotTestBasic struct {
|
|
||||||
scheme string // Disk scheme used for storing trie nodes
|
|
||||||
chainBlocks int // Number of blocks to generate for the canonical chain
|
|
||||||
snapshotBlock uint64 // Block number of the relevant snapshot disk layer
|
|
||||||
commitBlock uint64 // Block number for which to commit the state to disk
|
|
||||||
|
|
||||||
expCanonicalBlocks int // Number of canonical blocks expected to remain in the database (excl. genesis)
|
|
||||||
expHeadHeader uint64 // Block number of the expected head header
|
|
||||||
expHeadFastBlock uint64 // Block number of the expected head fast sync block
|
|
||||||
expHeadBlock uint64 // Block number of the expected head full block
|
|
||||||
expSnapshotBottom uint64 // The block height corresponding to the snapshot disk layer
|
|
||||||
|
|
||||||
// share fields, set in runtime
|
|
||||||
datadir string
|
|
||||||
ancient string
|
|
||||||
db ethdb.Database
|
|
||||||
genDb ethdb.Database
|
|
||||||
engine consensus.Engine
|
|
||||||
gspec *Genesis
|
|
||||||
}
|
|
||||||
|
|
||||||
func (basic *snapshotTestBasic) prepare(t *testing.T) (*BlockChain, []*types.Block) {
|
|
||||||
// Create a temporary persistent database
|
|
||||||
datadir := t.TempDir()
|
|
||||||
ancient := path.Join(datadir, "ancient")
|
|
||||||
|
|
||||||
db, err := rawdb.Open(rawdb.OpenOptions{
|
|
||||||
Directory: datadir,
|
|
||||||
AncientsDirectory: ancient,
|
|
||||||
Ephemeral: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create persistent database: %v", err)
|
|
||||||
}
|
|
||||||
// Initialize a fresh chain
|
|
||||||
var (
|
|
||||||
gspec = &Genesis{
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Config: params.AllEthashProtocolChanges,
|
|
||||||
}
|
|
||||||
engine = ethash.NewFullFaker()
|
|
||||||
)
|
|
||||||
chain, err := NewBlockChain(db, DefaultCacheConfigWithScheme(basic.scheme), gspec, nil, engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create chain: %v", err)
|
|
||||||
}
|
|
||||||
genDb, blocks, _ := GenerateChainWithGenesis(gspec, engine, basic.chainBlocks, func(i int, b *BlockGen) {})
|
|
||||||
|
|
||||||
// Insert the blocks with configured settings.
|
|
||||||
var breakpoints []uint64
|
|
||||||
if basic.commitBlock > basic.snapshotBlock {
|
|
||||||
breakpoints = append(breakpoints, basic.snapshotBlock, basic.commitBlock)
|
|
||||||
} else {
|
|
||||||
breakpoints = append(breakpoints, basic.commitBlock, basic.snapshotBlock)
|
|
||||||
}
|
|
||||||
var startPoint uint64
|
|
||||||
for _, point := range breakpoints {
|
|
||||||
if _, err := chain.InsertChain(blocks[startPoint:point]); err != nil {
|
|
||||||
t.Fatalf("Failed to import canonical chain start: %v", err)
|
|
||||||
}
|
|
||||||
startPoint = point
|
|
||||||
|
|
||||||
if basic.commitBlock > 0 && basic.commitBlock == point {
|
|
||||||
chain.TrieDB().Commit(blocks[point-1].Root(), false)
|
|
||||||
}
|
|
||||||
if basic.snapshotBlock > 0 && basic.snapshotBlock == point {
|
|
||||||
// Flushing the entire snap tree into the disk, the
|
|
||||||
// relevant (a) snapshot root and (b) snapshot generator
|
|
||||||
// will be persisted atomically.
|
|
||||||
chain.snaps.Cap(blocks[point-1].Root(), 0)
|
|
||||||
diskRoot, blockRoot := chain.snaps.DiskRoot(), blocks[point-1].Root()
|
|
||||||
if !bytes.Equal(diskRoot.Bytes(), blockRoot.Bytes()) {
|
|
||||||
t.Fatalf("Failed to flush disk layer change, want %x, got %x", blockRoot, diskRoot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := chain.InsertChain(blocks[startPoint:]); err != nil {
|
|
||||||
t.Fatalf("Failed to import canonical chain tail: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set runtime fields
|
|
||||||
basic.datadir = datadir
|
|
||||||
basic.ancient = ancient
|
|
||||||
basic.db = db
|
|
||||||
basic.genDb = genDb
|
|
||||||
basic.engine = engine
|
|
||||||
basic.gspec = gspec
|
|
||||||
return chain, blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
func (basic *snapshotTestBasic) verify(t *testing.T, chain *BlockChain, blocks []*types.Block) {
|
|
||||||
// Iterate over all the remaining blocks and ensure there are no gaps
|
|
||||||
verifyNoGaps(t, chain, true, blocks)
|
|
||||||
verifyCutoff(t, chain, true, blocks, basic.expCanonicalBlocks)
|
|
||||||
|
|
||||||
if head := chain.CurrentHeader(); head.Number.Uint64() != basic.expHeadHeader {
|
|
||||||
t.Errorf("Head header mismatch: have %d, want %d", head.Number, basic.expHeadHeader)
|
|
||||||
}
|
|
||||||
if head := chain.CurrentSnapBlock(); head.Number.Uint64() != basic.expHeadFastBlock {
|
|
||||||
t.Errorf("Head fast block mismatch: have %d, want %d", head.Number, basic.expHeadFastBlock)
|
|
||||||
}
|
|
||||||
if head := chain.CurrentBlock(); head.Number.Uint64() != basic.expHeadBlock {
|
|
||||||
t.Errorf("Head block mismatch: have %d, want %d", head.Number, basic.expHeadBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the disk layer, ensure they are matched
|
|
||||||
block := chain.GetBlockByNumber(basic.expSnapshotBottom)
|
|
||||||
if block == nil {
|
|
||||||
t.Errorf("The corresponding block[%d] of snapshot disk layer is missing", basic.expSnapshotBottom)
|
|
||||||
} else if !bytes.Equal(chain.snaps.DiskRoot().Bytes(), block.Root().Bytes()) {
|
|
||||||
t.Errorf("The snapshot disk layer root is incorrect, want %x, get %x", block.Root(), chain.snaps.DiskRoot())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the snapshot, ensure it's integrated
|
|
||||||
if err := chain.snaps.Verify(block.Root()); err != nil {
|
|
||||||
t.Errorf("The disk layer is not integrated %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:unused
|
|
||||||
func (basic *snapshotTestBasic) dump() string {
|
|
||||||
buffer := new(strings.Builder)
|
|
||||||
|
|
||||||
fmt.Fprint(buffer, "Chain:\n G")
|
|
||||||
for i := 0; i < basic.chainBlocks; i++ {
|
|
||||||
fmt.Fprintf(buffer, "->C%d", i+1)
|
|
||||||
}
|
|
||||||
fmt.Fprint(buffer, " (HEAD)\n\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(buffer, "Commit: G")
|
|
||||||
if basic.commitBlock > 0 {
|
|
||||||
fmt.Fprintf(buffer, ", C%d", basic.commitBlock)
|
|
||||||
}
|
|
||||||
fmt.Fprint(buffer, "\n")
|
|
||||||
|
|
||||||
fmt.Fprintf(buffer, "Snapshot: G")
|
|
||||||
if basic.snapshotBlock > 0 {
|
|
||||||
fmt.Fprintf(buffer, ", C%d", basic.snapshotBlock)
|
|
||||||
}
|
|
||||||
fmt.Fprint(buffer, "\n")
|
|
||||||
|
|
||||||
//if crash {
|
|
||||||
// fmt.Fprintf(buffer, "\nCRASH\n\n")
|
|
||||||
//} else {
|
|
||||||
// fmt.Fprintf(buffer, "\nSetHead(%d)\n\n", basic.setHead)
|
|
||||||
//}
|
|
||||||
fmt.Fprintf(buffer, "------------------------------\n\n")
|
|
||||||
|
|
||||||
fmt.Fprint(buffer, "Expected in leveldb:\n G")
|
|
||||||
for i := 0; i < basic.expCanonicalBlocks; i++ {
|
|
||||||
fmt.Fprintf(buffer, "->C%d", i+1)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(buffer, "\n\n")
|
|
||||||
fmt.Fprintf(buffer, "Expected head header : C%d\n", basic.expHeadHeader)
|
|
||||||
fmt.Fprintf(buffer, "Expected head fast block: C%d\n", basic.expHeadFastBlock)
|
|
||||||
if basic.expHeadBlock == 0 {
|
|
||||||
fmt.Fprintf(buffer, "Expected head block : G\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(buffer, "Expected head block : C%d\n", basic.expHeadBlock)
|
|
||||||
}
|
|
||||||
if basic.expSnapshotBottom == 0 {
|
|
||||||
fmt.Fprintf(buffer, "Expected snapshot disk : G\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(buffer, "Expected snapshot disk : C%d\n", basic.expSnapshotBottom)
|
|
||||||
}
|
|
||||||
return buffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (basic *snapshotTestBasic) teardown() {
|
|
||||||
basic.db.Close()
|
|
||||||
basic.genDb.Close()
|
|
||||||
os.RemoveAll(basic.datadir)
|
|
||||||
os.RemoveAll(basic.ancient)
|
|
||||||
}
|
|
||||||
|
|
||||||
// snapshotTest is a test case type for normal snapshot recovery.
|
|
||||||
// It can be used for testing that restart Geth normally.
|
|
||||||
type snapshotTest struct {
|
|
||||||
snapshotTestBasic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (snaptest *snapshotTest) test(t *testing.T) {
|
|
||||||
// It's hard to follow the test case, visualize the input
|
|
||||||
// log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
|
||||||
// fmt.Println(tt.dump())
|
|
||||||
chain, blocks := snaptest.prepare(t)
|
|
||||||
|
|
||||||
// Restart the chain normally
|
|
||||||
chain.Stop()
|
|
||||||
newchain, err := NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
defer newchain.Stop()
|
|
||||||
|
|
||||||
snaptest.verify(t, newchain, blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// crashSnapshotTest is a test case type for irregular snapshot recovery.
|
|
||||||
// It can be used for testing that restart Geth after the crash.
|
|
||||||
type crashSnapshotTest struct {
|
|
||||||
snapshotTestBasic
|
|
||||||
}
|
|
||||||
|
|
||||||
func (snaptest *crashSnapshotTest) test(t *testing.T) {
|
|
||||||
// It's hard to follow the test case, visualize the input
|
|
||||||
// log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
|
||||||
// fmt.Println(tt.dump())
|
|
||||||
chain, blocks := snaptest.prepare(t)
|
|
||||||
|
|
||||||
// Pull the plug on the database, simulating a hard crash
|
|
||||||
db := chain.db
|
|
||||||
db.Close()
|
|
||||||
chain.stopWithoutSaving()
|
|
||||||
chain.triedb.Close()
|
|
||||||
|
|
||||||
// Start a new blockchain back up and see where the repair leads us
|
|
||||||
newdb, err := rawdb.Open(rawdb.OpenOptions{
|
|
||||||
Directory: snaptest.datadir,
|
|
||||||
AncientsDirectory: snaptest.ancient,
|
|
||||||
Ephemeral: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to reopen persistent database: %v", err)
|
|
||||||
}
|
|
||||||
defer newdb.Close()
|
|
||||||
|
|
||||||
// The interesting thing is: instead of starting the blockchain after
|
|
||||||
// the crash, we do restart twice here: one after the crash and one
|
|
||||||
// after the normal stop. It's used to ensure the broken snapshot
|
|
||||||
// can be detected all the time.
|
|
||||||
newchain, err := NewBlockChain(newdb, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
newchain.Stop()
|
|
||||||
|
|
||||||
newchain, err = NewBlockChain(newdb, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
defer newchain.Stop()
|
|
||||||
|
|
||||||
snaptest.verify(t, newchain, blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// gappedSnapshotTest is a test type used to test this scenario:
|
|
||||||
// - have a complete snapshot
|
|
||||||
// - restart without enabling the snapshot
|
|
||||||
// - insert a few blocks
|
|
||||||
// - restart with enabling the snapshot again
|
|
||||||
type gappedSnapshotTest struct {
|
|
||||||
snapshotTestBasic
|
|
||||||
gapped int // Number of blocks to insert without enabling snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
func (snaptest *gappedSnapshotTest) test(t *testing.T) {
|
|
||||||
// It's hard to follow the test case, visualize the input
|
|
||||||
// log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
|
||||||
// fmt.Println(tt.dump())
|
|
||||||
chain, blocks := snaptest.prepare(t)
|
|
||||||
|
|
||||||
// Insert blocks without enabling snapshot if gapping is required.
|
|
||||||
chain.Stop()
|
|
||||||
gappedBlocks, _ := GenerateChain(snaptest.gspec.Config, blocks[len(blocks)-1], snaptest.engine, snaptest.genDb, snaptest.gapped, func(i int, b *BlockGen) {})
|
|
||||||
|
|
||||||
// Insert a few more blocks without enabling snapshot
|
|
||||||
var cacheConfig = &CacheConfig{
|
|
||||||
TrieCleanLimit: 256,
|
|
||||||
TrieDirtyLimit: 256,
|
|
||||||
TrieTimeLimit: 5 * time.Minute,
|
|
||||||
SnapshotLimit: 0,
|
|
||||||
StateScheme: snaptest.scheme,
|
|
||||||
}
|
|
||||||
newchain, err := NewBlockChain(snaptest.db, cacheConfig, snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
newchain.InsertChain(gappedBlocks)
|
|
||||||
newchain.Stop()
|
|
||||||
|
|
||||||
// Restart the chain with enabling the snapshot
|
|
||||||
newchain, err = NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
defer newchain.Stop()
|
|
||||||
|
|
||||||
snaptest.verify(t, newchain, blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setHeadSnapshotTest is the test type used to test this scenario:
|
|
||||||
// - have a complete snapshot
|
|
||||||
// - set the head to a lower point
|
|
||||||
// - restart
|
|
||||||
type setHeadSnapshotTest struct {
|
|
||||||
snapshotTestBasic
|
|
||||||
setHead uint64 // Block number to set head back to
|
|
||||||
}
|
|
||||||
|
|
||||||
func (snaptest *setHeadSnapshotTest) test(t *testing.T) {
|
|
||||||
// It's hard to follow the test case, visualize the input
|
|
||||||
// log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
|
||||||
// fmt.Println(tt.dump())
|
|
||||||
chain, blocks := snaptest.prepare(t)
|
|
||||||
|
|
||||||
// Rewind the chain if setHead operation is required.
|
|
||||||
chain.SetHead(snaptest.setHead)
|
|
||||||
chain.Stop()
|
|
||||||
|
|
||||||
newchain, err := NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
defer newchain.Stop()
|
|
||||||
|
|
||||||
snaptest.verify(t, newchain, blocks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wipeCrashSnapshotTest is the test type used to test this scenario:
|
|
||||||
// - have a complete snapshot
|
|
||||||
// - restart, insert more blocks without enabling the snapshot
|
|
||||||
// - restart again with enabling the snapshot
|
|
||||||
// - crash
|
|
||||||
type wipeCrashSnapshotTest struct {
|
|
||||||
snapshotTestBasic
|
|
||||||
newBlocks int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (snaptest *wipeCrashSnapshotTest) test(t *testing.T) {
|
|
||||||
// It's hard to follow the test case, visualize the input
|
|
||||||
// log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
|
|
||||||
// fmt.Println(tt.dump())
|
|
||||||
chain, blocks := snaptest.prepare(t)
|
|
||||||
|
|
||||||
// Firstly, stop the chain properly, with all snapshot journal
|
|
||||||
// and state committed.
|
|
||||||
chain.Stop()
|
|
||||||
|
|
||||||
config := &CacheConfig{
|
|
||||||
TrieCleanLimit: 256,
|
|
||||||
TrieDirtyLimit: 256,
|
|
||||||
TrieTimeLimit: 5 * time.Minute,
|
|
||||||
SnapshotLimit: 0,
|
|
||||||
StateScheme: snaptest.scheme,
|
|
||||||
}
|
|
||||||
newchain, err := NewBlockChain(snaptest.db, config, snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
newBlocks, _ := GenerateChain(snaptest.gspec.Config, blocks[len(blocks)-1], snaptest.engine, snaptest.genDb, snaptest.newBlocks, func(i int, b *BlockGen) {})
|
|
||||||
newchain.InsertChain(newBlocks)
|
|
||||||
newchain.Stop()
|
|
||||||
|
|
||||||
// Restart the chain, the wiper should start working
|
|
||||||
config = &CacheConfig{
|
|
||||||
TrieCleanLimit: 256,
|
|
||||||
TrieDirtyLimit: 256,
|
|
||||||
TrieTimeLimit: 5 * time.Minute,
|
|
||||||
SnapshotLimit: 256,
|
|
||||||
SnapshotWait: false, // Don't wait rebuild
|
|
||||||
StateScheme: snaptest.scheme,
|
|
||||||
}
|
|
||||||
tmp, err := NewBlockChain(snaptest.db, config, snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate the blockchain crash.
|
|
||||||
tmp.triedb.Close()
|
|
||||||
tmp.stopWithoutSaving()
|
|
||||||
|
|
||||||
newchain, err = NewBlockChain(snaptest.db, DefaultCacheConfigWithScheme(snaptest.scheme), snaptest.gspec, nil, snaptest.engine, vm.Config{}, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to recreate chain: %v", err)
|
|
||||||
}
|
|
||||||
snaptest.verify(t, newchain, blocks)
|
|
||||||
newchain.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests a Geth restart with valid snapshot. Before the shutdown, all snapshot
|
|
||||||
// journal will be persisted correctly. In this case no snapshot recovery is
|
|
||||||
// required.
|
|
||||||
func TestRestartWithNewSnapshot(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G
|
|
||||||
// Snapshot: G
|
|
||||||
//
|
|
||||||
// SetHead(0)
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8
|
|
||||||
//
|
|
||||||
// Expected head header : C8
|
|
||||||
// Expected head fast block: C8
|
|
||||||
// Expected head block : C8
|
|
||||||
// Expected snapshot disk : G
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
test := &snapshotTest{
|
|
||||||
snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 0,
|
|
||||||
commitBlock: 0,
|
|
||||||
expCanonicalBlocks: 8,
|
|
||||||
expHeadHeader: 8,
|
|
||||||
expHeadFastBlock: 8,
|
|
||||||
expHeadBlock: 8,
|
|
||||||
expSnapshotBottom: 0, // Initial disk layer built from genesis
|
|
||||||
},
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests a Geth was crashed and restarts with a broken snapshot. In this case the
|
|
||||||
// chain head should be rewound to the point with available state. And also the
|
|
||||||
// new head should must be lower than disk layer. But there is no committed point
|
|
||||||
// so the chain should be rewound to genesis and the disk layer should be left
|
|
||||||
// for recovery.
|
|
||||||
func TestNoCommitCrashWithNewSnapshot(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G
|
|
||||||
// Snapshot: G, C4
|
|
||||||
//
|
|
||||||
// CRASH
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8
|
|
||||||
//
|
|
||||||
// Expected head header : C8
|
|
||||||
// Expected head fast block: C8
|
|
||||||
// Expected head block : G
|
|
||||||
// Expected snapshot disk : C4
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
test := &crashSnapshotTest{
|
|
||||||
snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 4,
|
|
||||||
commitBlock: 0,
|
|
||||||
expCanonicalBlocks: 8,
|
|
||||||
expHeadHeader: 8,
|
|
||||||
expHeadFastBlock: 8,
|
|
||||||
expHeadBlock: 0,
|
|
||||||
expSnapshotBottom: 4, // Last committed disk layer, wait recovery
|
|
||||||
},
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests a Geth was crashed and restarts with a broken snapshot. In this case the
|
|
||||||
// chain head should be rewound to the point with available state. And also the
|
|
||||||
// new head should must be lower than disk layer. But there is only a low committed
|
|
||||||
// point so the chain should be rewound to committed point and the disk layer
|
|
||||||
// should be left for recovery.
|
|
||||||
func TestLowCommitCrashWithNewSnapshot(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G, C2
|
|
||||||
// Snapshot: G, C4
|
|
||||||
//
|
|
||||||
// CRASH
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8
|
|
||||||
//
|
|
||||||
// Expected head header : C8
|
|
||||||
// Expected head fast block: C8
|
|
||||||
// Expected head block : C2
|
|
||||||
// Expected snapshot disk : C4
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
test := &crashSnapshotTest{
|
|
||||||
snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 4,
|
|
||||||
commitBlock: 2,
|
|
||||||
expCanonicalBlocks: 8,
|
|
||||||
expHeadHeader: 8,
|
|
||||||
expHeadFastBlock: 8,
|
|
||||||
expHeadBlock: 2,
|
|
||||||
expSnapshotBottom: 4, // Last committed disk layer, wait recovery
|
|
||||||
},
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests a Geth was crashed and restarts with a broken snapshot. In this case
|
|
||||||
// the chain head should be rewound to the point with available state. And also
|
|
||||||
// the new head should must be lower than disk layer. But there is only a high
|
|
||||||
// committed point so the chain should be rewound to genesis and the disk layer
|
|
||||||
// should be left for recovery.
|
|
||||||
func TestHighCommitCrashWithNewSnapshot(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G, C6
|
|
||||||
// Snapshot: G, C4
|
|
||||||
//
|
|
||||||
// CRASH
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8
|
|
||||||
//
|
|
||||||
// Expected head header : C8
|
|
||||||
// Expected head fast block: C8
|
|
||||||
// Expected head block : G
|
|
||||||
// Expected snapshot disk : C4
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
expHead := uint64(0)
|
|
||||||
if scheme == rawdb.PathScheme {
|
|
||||||
expHead = uint64(4)
|
|
||||||
}
|
|
||||||
test := &crashSnapshotTest{
|
|
||||||
snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 4,
|
|
||||||
commitBlock: 6,
|
|
||||||
expCanonicalBlocks: 8,
|
|
||||||
expHeadHeader: 8,
|
|
||||||
expHeadFastBlock: 8,
|
|
||||||
expHeadBlock: expHead,
|
|
||||||
expSnapshotBottom: 4, // Last committed disk layer, wait recovery
|
|
||||||
},
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests a Geth was running with snapshot enabled. Then restarts without
|
|
||||||
// enabling snapshot and after that re-enable the snapshot again. In this
|
|
||||||
// case the snapshot should be rebuilt with latest chain head.
|
|
||||||
func TestGappedNewSnapshot(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G
|
|
||||||
// Snapshot: G
|
|
||||||
//
|
|
||||||
// SetHead(0)
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10
|
|
||||||
//
|
|
||||||
// Expected head header : C10
|
|
||||||
// Expected head fast block: C10
|
|
||||||
// Expected head block : C10
|
|
||||||
// Expected snapshot disk : C10
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
test := &gappedSnapshotTest{
|
|
||||||
snapshotTestBasic: snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 0,
|
|
||||||
commitBlock: 0,
|
|
||||||
expCanonicalBlocks: 10,
|
|
||||||
expHeadHeader: 10,
|
|
||||||
expHeadFastBlock: 10,
|
|
||||||
expHeadBlock: 10,
|
|
||||||
expSnapshotBottom: 10, // Rebuilt snapshot from the latest HEAD
|
|
||||||
},
|
|
||||||
gapped: 2,
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests the Geth was running with snapshot enabled and resetHead is applied.
|
|
||||||
// In this case the head is rewound to the target(with state available). After
|
|
||||||
// that the chain is restarted and the original disk layer is kept.
|
|
||||||
func TestSetHeadWithNewSnapshot(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G
|
|
||||||
// Snapshot: G
|
|
||||||
//
|
|
||||||
// SetHead(4)
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4
|
|
||||||
//
|
|
||||||
// Expected head header : C4
|
|
||||||
// Expected head fast block: C4
|
|
||||||
// Expected head block : C4
|
|
||||||
// Expected snapshot disk : G
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
test := &setHeadSnapshotTest{
|
|
||||||
snapshotTestBasic: snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 0,
|
|
||||||
commitBlock: 0,
|
|
||||||
expCanonicalBlocks: 4,
|
|
||||||
expHeadHeader: 4,
|
|
||||||
expHeadFastBlock: 4,
|
|
||||||
expHeadBlock: 4,
|
|
||||||
expSnapshotBottom: 0, // The initial disk layer is built from the genesis
|
|
||||||
},
|
|
||||||
setHead: 4,
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests the Geth was running with a complete snapshot and then imports a few
|
|
||||||
// more new blocks on top without enabling the snapshot. After the restart,
|
|
||||||
// crash happens. Check everything is ok after the restart.
|
|
||||||
func TestRecoverSnapshotFromWipingCrash(t *testing.T) {
|
|
||||||
// Chain:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD)
|
|
||||||
//
|
|
||||||
// Commit: G
|
|
||||||
// Snapshot: G
|
|
||||||
//
|
|
||||||
// SetHead(0)
|
|
||||||
//
|
|
||||||
// ------------------------------
|
|
||||||
//
|
|
||||||
// Expected in leveldb:
|
|
||||||
// G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10
|
|
||||||
//
|
|
||||||
// Expected head header : C10
|
|
||||||
// Expected head fast block: C10
|
|
||||||
// Expected head block : C8
|
|
||||||
// Expected snapshot disk : C10
|
|
||||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
|
||||||
test := &wipeCrashSnapshotTest{
|
|
||||||
snapshotTestBasic: snapshotTestBasic{
|
|
||||||
scheme: scheme,
|
|
||||||
chainBlocks: 8,
|
|
||||||
snapshotBlock: 4,
|
|
||||||
commitBlock: 0,
|
|
||||||
expCanonicalBlocks: 10,
|
|
||||||
expHeadHeader: 10,
|
|
||||||
expHeadFastBlock: 10,
|
|
||||||
expHeadBlock: 10,
|
|
||||||
expSnapshotBottom: 10,
|
|
||||||
},
|
|
||||||
newBlocks: 2,
|
|
||||||
}
|
|
||||||
test.test(t)
|
|
||||||
test.teardown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import "github.com/ethereum/go-ethereum/common"
|
|
||||||
|
|
||||||
// BadHashes represent a set of manually tracked bad hashes (usually hard forks)
|
|
||||||
var BadHashes = map[common.Hash]bool{
|
|
||||||
common.HexToHash("05bef30ef572270f654746da22639a7a0c97dd97a7050b9e252391996aaeb689"): true,
|
|
||||||
common.HexToHash("7d05d08cbc596a2e5e4f13b80a743e53e09221b5323c3a61946b20873e58583f"): true,
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/bitutil"
|
|
||||||
"github.com/ethereum/go-ethereum/core/bloombits"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// bloomThrottling is the time to wait between processing two consecutive index
|
|
||||||
// sections. It's useful during chain upgrades to prevent disk overload.
|
|
||||||
bloomThrottling = 100 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// BloomIndexer implements a core.ChainIndexer, building up a rotated bloom bits index
|
|
||||||
// for the Ethereum header bloom filters, permitting blazing fast filtering.
|
|
||||||
type BloomIndexer struct {
|
|
||||||
size uint64 // section size to generate bloombits for
|
|
||||||
db ethdb.Database // database instance to write index data and metadata into
|
|
||||||
gen *bloombits.Generator // generator to rotate the bloom bits crating the bloom index
|
|
||||||
section uint64 // Section is the section number being processed currently
|
|
||||||
head common.Hash // Head is the hash of the last header processed
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBloomIndexer returns a chain indexer that generates bloom bits data for the
|
|
||||||
// canonical chain for fast logs filtering.
|
|
||||||
func NewBloomIndexer(db ethdb.Database, size, confirms uint64) *ChainIndexer {
|
|
||||||
backend := &BloomIndexer{
|
|
||||||
db: db,
|
|
||||||
size: size,
|
|
||||||
}
|
|
||||||
table := rawdb.NewTable(db, string(rawdb.BloomBitsIndexPrefix))
|
|
||||||
|
|
||||||
return NewChainIndexer(db, table, backend, size, confirms, bloomThrottling, "bloombits")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset implements core.ChainIndexerBackend, starting a new bloombits index
|
|
||||||
// section.
|
|
||||||
func (b *BloomIndexer) Reset(ctx context.Context, section uint64, lastSectionHead common.Hash) error {
|
|
||||||
gen, err := bloombits.NewGenerator(uint(b.size))
|
|
||||||
b.gen, b.section, b.head = gen, section, common.Hash{}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process implements core.ChainIndexerBackend, adding a new header's bloom into
|
|
||||||
// the index.
|
|
||||||
func (b *BloomIndexer) Process(ctx context.Context, header *types.Header) error {
|
|
||||||
b.gen.AddBloom(uint(header.Number.Uint64()-b.section*b.size), header.Bloom)
|
|
||||||
b.head = header.Hash()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit implements core.ChainIndexerBackend, finalizing the bloom section and
|
|
||||||
// writing it out into the database.
|
|
||||||
func (b *BloomIndexer) Commit() error {
|
|
||||||
batch := b.db.NewBatchWithSize((int(b.size) / 8) * types.BloomBitLength)
|
|
||||||
for i := 0; i < types.BloomBitLength; i++ {
|
|
||||||
bits, err := b.gen.Bitset(uint(i))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rawdb.WriteBloomBits(batch, uint(i), b.section, b.head, bitutil.CompressBytes(bits))
|
|
||||||
}
|
|
||||||
return batch.Write()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune returns an empty error since we don't support pruning here.
|
|
||||||
func (b *BloomIndexer) Prune(threshold uint64) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package bloombits implements bloom filtering on batches of data.
|
|
||||||
package bloombits
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bloombits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// errSectionOutOfBounds is returned if the user tried to add more bloom filters
|
|
||||||
// to the batch than available space, or if tries to retrieve above the capacity.
|
|
||||||
errSectionOutOfBounds = errors.New("section out of bounds")
|
|
||||||
|
|
||||||
// errBloomBitOutOfBounds is returned if the user tried to retrieve specified
|
|
||||||
// bit bloom above the capacity.
|
|
||||||
errBloomBitOutOfBounds = errors.New("bloom bit out of bounds")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generator takes a number of bloom filters and generates the rotated bloom bits
|
|
||||||
// to be used for batched filtering.
|
|
||||||
type Generator struct {
|
|
||||||
blooms [types.BloomBitLength][]byte // Rotated blooms for per-bit matching
|
|
||||||
sections uint // Number of sections to batch together
|
|
||||||
nextSec uint // Next section to set when adding a bloom
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGenerator creates a rotated bloom generator that can iteratively fill a
|
|
||||||
// batched bloom filter's bits.
|
|
||||||
func NewGenerator(sections uint) (*Generator, error) {
|
|
||||||
if sections%8 != 0 {
|
|
||||||
return nil, errors.New("section count not multiple of 8")
|
|
||||||
}
|
|
||||||
b := &Generator{sections: sections}
|
|
||||||
for i := 0; i < types.BloomBitLength; i++ {
|
|
||||||
b.blooms[i] = make([]byte, sections/8)
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddBloom takes a single bloom filter and sets the corresponding bit column
|
|
||||||
// in memory accordingly.
|
|
||||||
func (b *Generator) AddBloom(index uint, bloom types.Bloom) error {
|
|
||||||
// Make sure we're not adding more bloom filters than our capacity
|
|
||||||
if b.nextSec >= b.sections {
|
|
||||||
return errSectionOutOfBounds
|
|
||||||
}
|
|
||||||
if b.nextSec != index {
|
|
||||||
return errors.New("bloom filter with unexpected index")
|
|
||||||
}
|
|
||||||
// Rotate the bloom and insert into our collection
|
|
||||||
byteIndex := b.nextSec / 8
|
|
||||||
bitIndex := byte(7 - b.nextSec%8)
|
|
||||||
for byt := 0; byt < types.BloomByteLength; byt++ {
|
|
||||||
bloomByte := bloom[types.BloomByteLength-1-byt]
|
|
||||||
if bloomByte == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
base := 8 * byt
|
|
||||||
b.blooms[base+7][byteIndex] |= ((bloomByte >> 7) & 1) << bitIndex
|
|
||||||
b.blooms[base+6][byteIndex] |= ((bloomByte >> 6) & 1) << bitIndex
|
|
||||||
b.blooms[base+5][byteIndex] |= ((bloomByte >> 5) & 1) << bitIndex
|
|
||||||
b.blooms[base+4][byteIndex] |= ((bloomByte >> 4) & 1) << bitIndex
|
|
||||||
b.blooms[base+3][byteIndex] |= ((bloomByte >> 3) & 1) << bitIndex
|
|
||||||
b.blooms[base+2][byteIndex] |= ((bloomByte >> 2) & 1) << bitIndex
|
|
||||||
b.blooms[base+1][byteIndex] |= ((bloomByte >> 1) & 1) << bitIndex
|
|
||||||
b.blooms[base][byteIndex] |= (bloomByte & 1) << bitIndex
|
|
||||||
}
|
|
||||||
b.nextSec++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bitset returns the bit vector belonging to the given bit index after all
|
|
||||||
// blooms have been added.
|
|
||||||
func (b *Generator) Bitset(idx uint) ([]byte, error) {
|
|
||||||
if b.nextSec != b.sections {
|
|
||||||
return nil, errors.New("bloom not fully generated yet")
|
|
||||||
}
|
|
||||||
if idx >= types.BloomBitLength {
|
|
||||||
return nil, errBloomBitOutOfBounds
|
|
||||||
}
|
|
||||||
return b.blooms[idx], nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bloombits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that batched bloom bits are correctly rotated from the input bloom
|
|
||||||
// filters.
|
|
||||||
func TestGenerator(t *testing.T) {
|
|
||||||
// Generate the input and the rotated output
|
|
||||||
var input, output [types.BloomBitLength][types.BloomByteLength]byte
|
|
||||||
|
|
||||||
for i := 0; i < types.BloomBitLength; i++ {
|
|
||||||
for j := 0; j < types.BloomBitLength; j++ {
|
|
||||||
bit := byte(rand.Int() % 2)
|
|
||||||
|
|
||||||
input[i][j/8] |= bit << byte(7-j%8)
|
|
||||||
output[types.BloomBitLength-1-j][i/8] |= bit << byte(7-i%8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Crunch the input through the generator and verify the result
|
|
||||||
gen, err := NewGenerator(types.BloomBitLength)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create bloombit generator: %v", err)
|
|
||||||
}
|
|
||||||
for i, bloom := range input {
|
|
||||||
if err := gen.AddBloom(uint(i), bloom); err != nil {
|
|
||||||
t.Fatalf("bloom %d: failed to add: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, want := range output {
|
|
||||||
have, err := gen.Bitset(uint(i))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("output %d: failed to retrieve bits: %v", i, err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(have, want[:]) {
|
|
||||||
t.Errorf("output %d: bit vector mismatch have %x, want %x", i, have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGenerator(b *testing.B) {
|
|
||||||
var input [types.BloomBitLength][types.BloomByteLength]byte
|
|
||||||
b.Run("empty", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
// Crunch the input through the generator and verify the result
|
|
||||||
gen, err := NewGenerator(types.BloomBitLength)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to create bloombit generator: %v", err)
|
|
||||||
}
|
|
||||||
for j, bloom := range &input {
|
|
||||||
if err := gen.AddBloom(uint(j), bloom); err != nil {
|
|
||||||
b.Fatalf("bloom %d: failed to add: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for i := 0; i < types.BloomBitLength; i++ {
|
|
||||||
crand.Read(input[i][:])
|
|
||||||
}
|
|
||||||
b.Run("random", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
// Crunch the input through the generator and verify the result
|
|
||||||
gen, err := NewGenerator(types.BloomBitLength)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to create bloombit generator: %v", err)
|
|
||||||
}
|
|
||||||
for j, bloom := range &input {
|
|
||||||
if err := gen.AddBloom(uint(j), bloom); err != nil {
|
|
||||||
b.Fatalf("bloom %d: failed to add: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,645 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bloombits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/bitutil"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// bloomIndexes represents the bit indexes inside the bloom filter that belong
|
|
||||||
// to some key.
|
|
||||||
type bloomIndexes [3]uint
|
|
||||||
|
|
||||||
// calcBloomIndexes returns the bloom filter bit indexes belonging to the given key.
|
|
||||||
func calcBloomIndexes(b []byte) bloomIndexes {
|
|
||||||
b = crypto.Keccak256(b)
|
|
||||||
|
|
||||||
var idxs bloomIndexes
|
|
||||||
for i := 0; i < len(idxs); i++ {
|
|
||||||
idxs[i] = (uint(b[2*i])<<8)&2047 + uint(b[2*i+1])
|
|
||||||
}
|
|
||||||
return idxs
|
|
||||||
}
|
|
||||||
|
|
||||||
// partialMatches with a non-nil vector represents a section in which some sub-
|
|
||||||
// matchers have already found potential matches. Subsequent sub-matchers will
|
|
||||||
// binary AND their matches with this vector. If vector is nil, it represents a
|
|
||||||
// section to be processed by the first sub-matcher.
|
|
||||||
type partialMatches struct {
|
|
||||||
section uint64
|
|
||||||
bitset []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieval represents a request for retrieval task assignments for a given
|
|
||||||
// bit with the given number of fetch elements, or a response for such a request.
|
|
||||||
// It can also have the actual results set to be used as a delivery data struct.
|
|
||||||
//
|
|
||||||
// The context and error fields are used by the light client to terminate matching
|
|
||||||
// early if an error is encountered on some path of the pipeline.
|
|
||||||
type Retrieval struct {
|
|
||||||
Bit uint
|
|
||||||
Sections []uint64
|
|
||||||
Bitsets [][]byte
|
|
||||||
|
|
||||||
Context context.Context
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matcher is a pipelined system of schedulers and logic matchers which perform
|
|
||||||
// binary AND/OR operations on the bit-streams, creating a stream of potential
|
|
||||||
// blocks to inspect for data content.
|
|
||||||
type Matcher struct {
|
|
||||||
sectionSize uint64 // Size of the data batches to filter on
|
|
||||||
|
|
||||||
filters [][]bloomIndexes // Filter the system is matching for
|
|
||||||
schedulers map[uint]*scheduler // Retrieval schedulers for loading bloom bits
|
|
||||||
|
|
||||||
retrievers chan chan uint // Retriever processes waiting for bit allocations
|
|
||||||
counters chan chan uint // Retriever processes waiting for task count reports
|
|
||||||
retrievals chan chan *Retrieval // Retriever processes waiting for task allocations
|
|
||||||
deliveries chan *Retrieval // Retriever processes waiting for task response deliveries
|
|
||||||
|
|
||||||
running atomic.Bool // Atomic flag whether a session is live or not
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMatcher creates a new pipeline for retrieving bloom bit streams and doing
|
|
||||||
// address and topic filtering on them. Setting a filter component to `nil` is
|
|
||||||
// allowed and will result in that filter rule being skipped (OR 0x11...1).
|
|
||||||
func NewMatcher(sectionSize uint64, filters [][][]byte) *Matcher {
|
|
||||||
// Create the matcher instance
|
|
||||||
m := &Matcher{
|
|
||||||
sectionSize: sectionSize,
|
|
||||||
schedulers: make(map[uint]*scheduler),
|
|
||||||
retrievers: make(chan chan uint),
|
|
||||||
counters: make(chan chan uint),
|
|
||||||
retrievals: make(chan chan *Retrieval),
|
|
||||||
deliveries: make(chan *Retrieval),
|
|
||||||
}
|
|
||||||
// Calculate the bloom bit indexes for the groups we're interested in
|
|
||||||
m.filters = nil
|
|
||||||
|
|
||||||
for _, filter := range filters {
|
|
||||||
// Gather the bit indexes of the filter rule, special casing the nil filter
|
|
||||||
if len(filter) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
bloomBits := make([]bloomIndexes, len(filter))
|
|
||||||
for i, clause := range filter {
|
|
||||||
if clause == nil {
|
|
||||||
bloomBits = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
bloomBits[i] = calcBloomIndexes(clause)
|
|
||||||
}
|
|
||||||
// Accumulate the filter rules if no nil rule was within
|
|
||||||
if bloomBits != nil {
|
|
||||||
m.filters = append(m.filters, bloomBits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For every bit, create a scheduler to load/download the bit vectors
|
|
||||||
for _, bloomIndexLists := range m.filters {
|
|
||||||
for _, bloomIndexList := range bloomIndexLists {
|
|
||||||
for _, bloomIndex := range bloomIndexList {
|
|
||||||
m.addScheduler(bloomIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// addScheduler adds a bit stream retrieval scheduler for the given bit index if
|
|
||||||
// it has not existed before. If the bit is already selected for filtering, the
|
|
||||||
// existing scheduler can be used.
|
|
||||||
func (m *Matcher) addScheduler(idx uint) {
|
|
||||||
if _, ok := m.schedulers[idx]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.schedulers[idx] = newScheduler(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the matching process and returns a stream of bloom matches in
|
|
||||||
// a given range of blocks. If there are no more matches in the range, the result
|
|
||||||
// channel is closed.
|
|
||||||
func (m *Matcher) Start(ctx context.Context, begin, end uint64, results chan uint64) (*MatcherSession, error) {
|
|
||||||
// Make sure we're not creating concurrent sessions
|
|
||||||
if m.running.Swap(true) {
|
|
||||||
return nil, errors.New("matcher already running")
|
|
||||||
}
|
|
||||||
defer m.running.Store(false)
|
|
||||||
|
|
||||||
// Initiate a new matching round
|
|
||||||
session := &MatcherSession{
|
|
||||||
matcher: m,
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
for _, scheduler := range m.schedulers {
|
|
||||||
scheduler.reset()
|
|
||||||
}
|
|
||||||
sink := m.run(begin, end, cap(results), session)
|
|
||||||
|
|
||||||
// Read the output from the result sink and deliver to the user
|
|
||||||
session.pend.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer session.pend.Done()
|
|
||||||
defer close(results)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case res, ok := <-sink:
|
|
||||||
// New match result found
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Calculate the first and last blocks of the section
|
|
||||||
sectionStart := res.section * m.sectionSize
|
|
||||||
|
|
||||||
first := sectionStart
|
|
||||||
if begin > first {
|
|
||||||
first = begin
|
|
||||||
}
|
|
||||||
last := sectionStart + m.sectionSize - 1
|
|
||||||
if end < last {
|
|
||||||
last = end
|
|
||||||
}
|
|
||||||
// Iterate over all the blocks in the section and return the matching ones
|
|
||||||
for i := first; i <= last; i++ {
|
|
||||||
// Skip the entire byte if no matches are found inside (and we're processing an entire byte!)
|
|
||||||
next := res.bitset[(i-sectionStart)/8]
|
|
||||||
if next == 0 {
|
|
||||||
if i%8 == 0 {
|
|
||||||
i += 7
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Some bit it set, do the actual submatching
|
|
||||||
if bit := 7 - i%8; next&(1<<bit) != 0 {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
case results <- i:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// run creates a daisy-chain of sub-matchers, one for the address set and one
|
|
||||||
// for each topic set, each sub-matcher receiving a section only if the previous
|
|
||||||
// ones have all found a potential match in one of the blocks of the section,
|
|
||||||
// then binary AND-ing its own matches and forwarding the result to the next one.
|
|
||||||
//
|
|
||||||
// The method starts feeding the section indexes into the first sub-matcher on a
|
|
||||||
// new goroutine and returns a sink channel receiving the results.
|
|
||||||
func (m *Matcher) run(begin, end uint64, buffer int, session *MatcherSession) chan *partialMatches {
|
|
||||||
// Create the source channel and feed section indexes into
|
|
||||||
source := make(chan *partialMatches, buffer)
|
|
||||||
|
|
||||||
session.pend.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer session.pend.Done()
|
|
||||||
defer close(source)
|
|
||||||
|
|
||||||
for i := begin / m.sectionSize; i <= end/m.sectionSize; i++ {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
case source <- &partialMatches{i, bytes.Repeat([]byte{0xff}, int(m.sectionSize/8))}:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Assemble the daisy-chained filtering pipeline
|
|
||||||
next := source
|
|
||||||
dist := make(chan *request, buffer)
|
|
||||||
|
|
||||||
for _, bloom := range m.filters {
|
|
||||||
next = m.subMatch(next, dist, bloom, session)
|
|
||||||
}
|
|
||||||
// Start the request distribution
|
|
||||||
session.pend.Add(1)
|
|
||||||
go m.distributor(dist, session)
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
// subMatch creates a sub-matcher that filters for a set of addresses or topics, binary OR-s those matches, then
|
|
||||||
// binary AND-s the result to the daisy-chain input (source) and forwards it to the daisy-chain output.
|
|
||||||
// The matches of each address/topic are calculated by fetching the given sections of the three bloom bit indexes belonging to
|
|
||||||
// that address/topic, and binary AND-ing those vectors together.
|
|
||||||
func (m *Matcher) subMatch(source chan *partialMatches, dist chan *request, bloom []bloomIndexes, session *MatcherSession) chan *partialMatches {
|
|
||||||
// Start the concurrent schedulers for each bit required by the bloom filter
|
|
||||||
sectionSources := make([][3]chan uint64, len(bloom))
|
|
||||||
sectionSinks := make([][3]chan []byte, len(bloom))
|
|
||||||
for i, bits := range bloom {
|
|
||||||
for j, bit := range bits {
|
|
||||||
sectionSources[i][j] = make(chan uint64, cap(source))
|
|
||||||
sectionSinks[i][j] = make(chan []byte, cap(source))
|
|
||||||
|
|
||||||
m.schedulers[bit].run(sectionSources[i][j], dist, sectionSinks[i][j], session.quit, &session.pend)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process := make(chan *partialMatches, cap(source)) // entries from source are forwarded here after fetches have been initiated
|
|
||||||
results := make(chan *partialMatches, cap(source))
|
|
||||||
|
|
||||||
session.pend.Add(2)
|
|
||||||
go func() {
|
|
||||||
// Tear down the goroutine and terminate all source channels
|
|
||||||
defer session.pend.Done()
|
|
||||||
defer close(process)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for _, bloomSources := range sectionSources {
|
|
||||||
for _, bitSource := range bloomSources {
|
|
||||||
close(bitSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Read sections from the source channel and multiplex into all bit-schedulers
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case subres, ok := <-source:
|
|
||||||
// New subresult from previous link
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Multiplex the section index to all bit-schedulers
|
|
||||||
for _, bloomSources := range sectionSources {
|
|
||||||
for _, bitSource := range bloomSources {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
case bitSource <- subres.section:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Notify the processor that this section will become available
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
case process <- subres:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
// Tear down the goroutine and terminate the final sink channel
|
|
||||||
defer session.pend.Done()
|
|
||||||
defer close(results)
|
|
||||||
|
|
||||||
// Read the source notifications and collect the delivered results
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case subres, ok := <-process:
|
|
||||||
// Notified of a section being retrieved
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Gather all the sub-results and merge them together
|
|
||||||
var orVector []byte
|
|
||||||
for _, bloomSinks := range sectionSinks {
|
|
||||||
var andVector []byte
|
|
||||||
for _, bitSink := range bloomSinks {
|
|
||||||
var data []byte
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
case data = <-bitSink:
|
|
||||||
}
|
|
||||||
if andVector == nil {
|
|
||||||
andVector = make([]byte, int(m.sectionSize/8))
|
|
||||||
copy(andVector, data)
|
|
||||||
} else {
|
|
||||||
bitutil.ANDBytes(andVector, andVector, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if orVector == nil {
|
|
||||||
orVector = andVector
|
|
||||||
} else {
|
|
||||||
bitutil.ORBytes(orVector, orVector, andVector)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if orVector == nil {
|
|
||||||
orVector = make([]byte, int(m.sectionSize/8))
|
|
||||||
}
|
|
||||||
if subres.bitset != nil {
|
|
||||||
bitutil.ANDBytes(orVector, orVector, subres.bitset)
|
|
||||||
}
|
|
||||||
if bitutil.TestBytes(orVector) {
|
|
||||||
select {
|
|
||||||
case <-session.quit:
|
|
||||||
return
|
|
||||||
case results <- &partialMatches{subres.section, orVector}:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// distributor receives requests from the schedulers and queues them into a set
|
|
||||||
// of pending requests, which are assigned to retrievers wanting to fulfil them.
|
|
||||||
func (m *Matcher) distributor(dist chan *request, session *MatcherSession) {
|
|
||||||
defer session.pend.Done()
|
|
||||||
|
|
||||||
var (
|
|
||||||
requests = make(map[uint][]uint64) // Per-bit list of section requests, ordered by section number
|
|
||||||
unallocs = make(map[uint]struct{}) // Bits with pending requests but not allocated to any retriever
|
|
||||||
retrievers chan chan uint // Waiting retrievers (toggled to nil if unallocs is empty)
|
|
||||||
allocs int // Number of active allocations to handle graceful shutdown requests
|
|
||||||
shutdown = session.quit // Shutdown request channel, will gracefully wait for pending requests
|
|
||||||
)
|
|
||||||
|
|
||||||
// assign is a helper method to try to assign a pending bit an actively
|
|
||||||
// listening servicer, or schedule it up for later when one arrives.
|
|
||||||
assign := func(bit uint) {
|
|
||||||
select {
|
|
||||||
case fetcher := <-m.retrievers:
|
|
||||||
allocs++
|
|
||||||
fetcher <- bit
|
|
||||||
default:
|
|
||||||
// No retrievers active, start listening for new ones
|
|
||||||
retrievers = m.retrievers
|
|
||||||
unallocs[bit] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-shutdown:
|
|
||||||
// Shutdown requested. No more retrievers can be allocated,
|
|
||||||
// but we still need to wait until all pending requests have returned.
|
|
||||||
shutdown = nil
|
|
||||||
if allocs == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case req := <-dist:
|
|
||||||
// New retrieval request arrived to be distributed to some fetcher process
|
|
||||||
queue := requests[req.bit]
|
|
||||||
index := sort.Search(len(queue), func(i int) bool { return queue[i] >= req.section })
|
|
||||||
requests[req.bit] = append(queue[:index], append([]uint64{req.section}, queue[index:]...)...)
|
|
||||||
|
|
||||||
// If it's a new bit and we have waiting fetchers, allocate to them
|
|
||||||
if len(queue) == 0 {
|
|
||||||
assign(req.bit)
|
|
||||||
}
|
|
||||||
|
|
||||||
case fetcher := <-retrievers:
|
|
||||||
// New retriever arrived, find the lowest section-ed bit to assign
|
|
||||||
bit, best := uint(0), uint64(math.MaxUint64)
|
|
||||||
for idx := range unallocs {
|
|
||||||
if requests[idx][0] < best {
|
|
||||||
bit, best = idx, requests[idx][0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Stop tracking this bit (and alloc notifications if no more work is available)
|
|
||||||
delete(unallocs, bit)
|
|
||||||
if len(unallocs) == 0 {
|
|
||||||
retrievers = nil
|
|
||||||
}
|
|
||||||
allocs++
|
|
||||||
fetcher <- bit
|
|
||||||
|
|
||||||
case fetcher := <-m.counters:
|
|
||||||
// New task count request arrives, return number of items
|
|
||||||
fetcher <- uint(len(requests[<-fetcher]))
|
|
||||||
|
|
||||||
case fetcher := <-m.retrievals:
|
|
||||||
// New fetcher waiting for tasks to retrieve, assign
|
|
||||||
task := <-fetcher
|
|
||||||
if want := len(task.Sections); want >= len(requests[task.Bit]) {
|
|
||||||
task.Sections = requests[task.Bit]
|
|
||||||
delete(requests, task.Bit)
|
|
||||||
} else {
|
|
||||||
task.Sections = append(task.Sections[:0], requests[task.Bit][:want]...)
|
|
||||||
requests[task.Bit] = append(requests[task.Bit][:0], requests[task.Bit][want:]...)
|
|
||||||
}
|
|
||||||
fetcher <- task
|
|
||||||
|
|
||||||
// If anything was left unallocated, try to assign to someone else
|
|
||||||
if len(requests[task.Bit]) > 0 {
|
|
||||||
assign(task.Bit)
|
|
||||||
}
|
|
||||||
|
|
||||||
case result := <-m.deliveries:
|
|
||||||
// New retrieval task response from fetcher, split out missing sections and
|
|
||||||
// deliver complete ones
|
|
||||||
var (
|
|
||||||
sections = make([]uint64, 0, len(result.Sections))
|
|
||||||
bitsets = make([][]byte, 0, len(result.Bitsets))
|
|
||||||
missing = make([]uint64, 0, len(result.Sections))
|
|
||||||
)
|
|
||||||
for i, bitset := range result.Bitsets {
|
|
||||||
if len(bitset) == 0 {
|
|
||||||
missing = append(missing, result.Sections[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sections = append(sections, result.Sections[i])
|
|
||||||
bitsets = append(bitsets, bitset)
|
|
||||||
}
|
|
||||||
m.schedulers[result.Bit].deliver(sections, bitsets)
|
|
||||||
allocs--
|
|
||||||
|
|
||||||
// Reschedule missing sections and allocate bit if newly available
|
|
||||||
if len(missing) > 0 {
|
|
||||||
queue := requests[result.Bit]
|
|
||||||
for _, section := range missing {
|
|
||||||
index := sort.Search(len(queue), func(i int) bool { return queue[i] >= section })
|
|
||||||
queue = append(queue[:index], append([]uint64{section}, queue[index:]...)...)
|
|
||||||
}
|
|
||||||
requests[result.Bit] = queue
|
|
||||||
|
|
||||||
if len(queue) == len(missing) {
|
|
||||||
assign(result.Bit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End the session when all pending deliveries have arrived.
|
|
||||||
if shutdown == nil && allocs == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatcherSession is returned by a started matcher to be used as a terminator
|
|
||||||
// for the actively running matching operation.
|
|
||||||
type MatcherSession struct {
|
|
||||||
matcher *Matcher
|
|
||||||
|
|
||||||
closer sync.Once // Sync object to ensure we only ever close once
|
|
||||||
quit chan struct{} // Quit channel to request pipeline termination
|
|
||||||
|
|
||||||
ctx context.Context // Context used by the light client to abort filtering
|
|
||||||
err error // Global error to track retrieval failures deep in the chain
|
|
||||||
errLock sync.Mutex
|
|
||||||
|
|
||||||
pend sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close stops the matching process and waits for all subprocesses to terminate
|
|
||||||
// before returning. The timeout may be used for graceful shutdown, allowing the
|
|
||||||
// currently running retrievals to complete before this time.
|
|
||||||
func (s *MatcherSession) Close() {
|
|
||||||
s.closer.Do(func() {
|
|
||||||
// Signal termination and wait for all goroutines to tear down
|
|
||||||
close(s.quit)
|
|
||||||
s.pend.Wait()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns any failure encountered during the matching session.
|
|
||||||
func (s *MatcherSession) Error() error {
|
|
||||||
s.errLock.Lock()
|
|
||||||
defer s.errLock.Unlock()
|
|
||||||
|
|
||||||
return s.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocateRetrieval assigns a bloom bit index to a client process that can either
|
|
||||||
// immediately request and fetch the section contents assigned to this bit or wait
|
|
||||||
// a little while for more sections to be requested.
|
|
||||||
func (s *MatcherSession) allocateRetrieval() (uint, bool) {
|
|
||||||
fetcher := make(chan uint)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-s.quit:
|
|
||||||
return 0, false
|
|
||||||
case s.matcher.retrievers <- fetcher:
|
|
||||||
bit, ok := <-fetcher
|
|
||||||
return bit, ok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pendingSections returns the number of pending section retrievals belonging to
|
|
||||||
// the given bloom bit index.
|
|
||||||
func (s *MatcherSession) pendingSections(bit uint) int {
|
|
||||||
fetcher := make(chan uint)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-s.quit:
|
|
||||||
return 0
|
|
||||||
case s.matcher.counters <- fetcher:
|
|
||||||
fetcher <- bit
|
|
||||||
return int(<-fetcher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocateSections assigns all or part of an already allocated bit-task queue
|
|
||||||
// to the requesting process.
|
|
||||||
func (s *MatcherSession) allocateSections(bit uint, count int) []uint64 {
|
|
||||||
fetcher := make(chan *Retrieval)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-s.quit:
|
|
||||||
return nil
|
|
||||||
case s.matcher.retrievals <- fetcher:
|
|
||||||
task := &Retrieval{
|
|
||||||
Bit: bit,
|
|
||||||
Sections: make([]uint64, count),
|
|
||||||
}
|
|
||||||
fetcher <- task
|
|
||||||
return (<-fetcher).Sections
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deliverSections delivers a batch of section bit-vectors for a specific bloom
|
|
||||||
// bit index to be injected into the processing pipeline.
|
|
||||||
func (s *MatcherSession) deliverSections(bit uint, sections []uint64, bitsets [][]byte) {
|
|
||||||
s.matcher.deliveries <- &Retrieval{Bit: bit, Sections: sections, Bitsets: bitsets}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiplex polls the matcher session for retrieval tasks and multiplexes it into
|
|
||||||
// the requested retrieval queue to be serviced together with other sessions.
|
|
||||||
//
|
|
||||||
// This method will block for the lifetime of the session. Even after termination
|
|
||||||
// of the session, any request in-flight need to be responded to! Empty responses
|
|
||||||
// are fine though in that case.
|
|
||||||
func (s *MatcherSession) Multiplex(batch int, wait time.Duration, mux chan chan *Retrieval) {
|
|
||||||
for {
|
|
||||||
// Allocate a new bloom bit index to retrieve data for, stopping when done
|
|
||||||
bit, ok := s.allocateRetrieval()
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Bit allocated, throttle a bit if we're below our batch limit
|
|
||||||
if s.pendingSections(bit) < batch {
|
|
||||||
select {
|
|
||||||
case <-s.quit:
|
|
||||||
// Session terminating, we can't meaningfully service, abort
|
|
||||||
s.allocateSections(bit, 0)
|
|
||||||
s.deliverSections(bit, []uint64{}, [][]byte{})
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-time.After(wait):
|
|
||||||
// Throttling up, fetch whatever is available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Allocate as much as we can handle and request servicing
|
|
||||||
sections := s.allocateSections(bit, batch)
|
|
||||||
request := make(chan *Retrieval)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-s.quit:
|
|
||||||
// Session terminating, we can't meaningfully service, abort
|
|
||||||
s.deliverSections(bit, sections, make([][]byte, len(sections)))
|
|
||||||
return
|
|
||||||
|
|
||||||
case mux <- request:
|
|
||||||
// Retrieval accepted, something must arrive before we're aborting
|
|
||||||
request <- &Retrieval{Bit: bit, Sections: sections, Context: s.ctx}
|
|
||||||
|
|
||||||
result := <-request
|
|
||||||
|
|
||||||
// Deliver a result before s.Close() to avoid a deadlock
|
|
||||||
s.deliverSections(result.Bit, result.Sections, result.Bitsets)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
s.errLock.Lock()
|
|
||||||
s.err = result.Error
|
|
||||||
s.errLock.Unlock()
|
|
||||||
s.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bloombits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
const testSectionSize = 4096
|
|
||||||
|
|
||||||
// Tests that wildcard filter rules (nil) can be specified and are handled well.
|
|
||||||
func TestMatcherWildcards(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
matcher := NewMatcher(testSectionSize, [][][]byte{
|
|
||||||
{common.Address{}.Bytes(), common.Address{0x01}.Bytes()}, // Default address is not a wildcard
|
|
||||||
{common.Hash{}.Bytes(), common.Hash{0x01}.Bytes()}, // Default hash is not a wildcard
|
|
||||||
{common.Hash{0x01}.Bytes()}, // Plain rule, sanity check
|
|
||||||
{common.Hash{0x01}.Bytes(), nil}, // Wildcard suffix, drop rule
|
|
||||||
{nil, common.Hash{0x01}.Bytes()}, // Wildcard prefix, drop rule
|
|
||||||
{nil, nil}, // Wildcard combo, drop rule
|
|
||||||
{}, // Inited wildcard rule, drop rule
|
|
||||||
nil, // Proper wildcard rule, drop rule
|
|
||||||
})
|
|
||||||
if len(matcher.filters) != 3 {
|
|
||||||
t.Fatalf("filter system size mismatch: have %d, want %d", len(matcher.filters), 3)
|
|
||||||
}
|
|
||||||
if len(matcher.filters[0]) != 2 {
|
|
||||||
t.Fatalf("address clause size mismatch: have %d, want %d", len(matcher.filters[0]), 2)
|
|
||||||
}
|
|
||||||
if len(matcher.filters[1]) != 2 {
|
|
||||||
t.Fatalf("combo topic clause size mismatch: have %d, want %d", len(matcher.filters[1]), 2)
|
|
||||||
}
|
|
||||||
if len(matcher.filters[2]) != 1 {
|
|
||||||
t.Fatalf("singletone topic clause size mismatch: have %d, want %d", len(matcher.filters[2]), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests the matcher pipeline on a single continuous workflow without interrupts.
|
|
||||||
func TestMatcherContinuous(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testMatcherDiffBatches(t, [][]bloomIndexes{{{10, 20, 30}}}, 0, 100000, false, 75)
|
|
||||||
testMatcherDiffBatches(t, [][]bloomIndexes{{{32, 3125, 100}}, {{40, 50, 10}}}, 0, 100000, false, 81)
|
|
||||||
testMatcherDiffBatches(t, [][]bloomIndexes{{{4, 8, 11}, {7, 8, 17}}, {{9, 9, 12}, {15, 20, 13}}, {{18, 15, 15}, {12, 10, 4}}}, 0, 10000, false, 36)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests the matcher pipeline on a constantly interrupted and resumed work pattern
|
|
||||||
// with the aim of ensuring data items are requested only once.
|
|
||||||
func TestMatcherIntermittent(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testMatcherDiffBatches(t, [][]bloomIndexes{{{10, 20, 30}}}, 0, 100000, true, 75)
|
|
||||||
testMatcherDiffBatches(t, [][]bloomIndexes{{{32, 3125, 100}}, {{40, 50, 10}}}, 0, 100000, true, 81)
|
|
||||||
testMatcherDiffBatches(t, [][]bloomIndexes{{{4, 8, 11}, {7, 8, 17}}, {{9, 9, 12}, {15, 20, 13}}, {{18, 15, 15}, {12, 10, 4}}}, 0, 10000, true, 36)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests the matcher pipeline on random input to hopefully catch anomalies.
|
|
||||||
func TestMatcherRandom(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
testMatcherBothModes(t, makeRandomIndexes([]int{1}, 50), 0, 10000, 0)
|
|
||||||
testMatcherBothModes(t, makeRandomIndexes([]int{3}, 50), 0, 10000, 0)
|
|
||||||
testMatcherBothModes(t, makeRandomIndexes([]int{2, 2, 2}, 20), 0, 10000, 0)
|
|
||||||
testMatcherBothModes(t, makeRandomIndexes([]int{5, 5, 5}, 50), 0, 10000, 0)
|
|
||||||
testMatcherBothModes(t, makeRandomIndexes([]int{4, 4, 4}, 20), 0, 10000, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that the matcher can properly find matches if the starting block is
|
|
||||||
// shifted from a multiple of 8. This is needed to cover an optimisation with
|
|
||||||
// bitset matching https://github.com/ethereum/go-ethereum/issues/15309.
|
|
||||||
func TestMatcherShifted(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// Block 0 always matches in the tests, skip ahead of first 8 blocks with the
|
|
||||||
// start to get a potential zero byte in the matcher bitset.
|
|
||||||
|
|
||||||
// To keep the second bitset byte zero, the filter must only match for the first
|
|
||||||
// time in block 16, so doing an all-16 bit filter should suffice.
|
|
||||||
|
|
||||||
// To keep the starting block non divisible by 8, block number 9 is the first
|
|
||||||
// that would introduce a shift and not match block 0.
|
|
||||||
testMatcherBothModes(t, [][]bloomIndexes{{{16, 16, 16}}}, 9, 64, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that matching on everything doesn't crash (special case internally).
|
|
||||||
func TestWildcardMatcher(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testMatcherBothModes(t, nil, 0, 10000, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeRandomIndexes generates a random filter system, composed of multiple filter
|
|
||||||
// criteria, each having one bloom list component for the address and arbitrarily
|
|
||||||
// many topic bloom list components.
|
|
||||||
func makeRandomIndexes(lengths []int, max int) [][]bloomIndexes {
|
|
||||||
res := make([][]bloomIndexes, len(lengths))
|
|
||||||
for i, topics := range lengths {
|
|
||||||
res[i] = make([]bloomIndexes, topics)
|
|
||||||
for j := 0; j < topics; j++ {
|
|
||||||
for k := 0; k < len(res[i][j]); k++ {
|
|
||||||
res[i][j][k] = uint(rand.Intn(max-1) + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMatcherDiffBatches runs the given matches test in single-delivery and also
|
|
||||||
// in batches delivery mode, verifying that all kinds of deliveries are handled
|
|
||||||
// correctly within.
|
|
||||||
func testMatcherDiffBatches(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, intermittent bool, retrievals uint32) {
|
|
||||||
singleton := testMatcher(t, filter, start, blocks, intermittent, retrievals, 1)
|
|
||||||
batched := testMatcher(t, filter, start, blocks, intermittent, retrievals, 16)
|
|
||||||
|
|
||||||
if singleton != batched {
|
|
||||||
t.Errorf("filter = %v blocks = %v intermittent = %v: request count mismatch, %v in singleton vs. %v in batched mode", filter, blocks, intermittent, singleton, batched)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMatcherBothModes runs the given matcher test in both continuous as well as
|
|
||||||
// in intermittent mode, verifying that the request counts match each other.
|
|
||||||
func testMatcherBothModes(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, retrievals uint32) {
|
|
||||||
continuous := testMatcher(t, filter, start, blocks, false, retrievals, 16)
|
|
||||||
intermittent := testMatcher(t, filter, start, blocks, true, retrievals, 16)
|
|
||||||
|
|
||||||
if continuous != intermittent {
|
|
||||||
t.Errorf("filter = %v blocks = %v: request count mismatch, %v in continuous vs. %v in intermittent mode", filter, blocks, continuous, intermittent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMatcher is a generic tester to run the given matcher test and return the
|
|
||||||
// number of requests made for cross validation between different modes.
|
|
||||||
func testMatcher(t *testing.T, filter [][]bloomIndexes, start, blocks uint64, intermittent bool, retrievals uint32, maxReqCount int) uint32 {
|
|
||||||
// Create a new matcher an simulate our explicit random bitsets
|
|
||||||
matcher := NewMatcher(testSectionSize, nil)
|
|
||||||
matcher.filters = filter
|
|
||||||
|
|
||||||
for _, rule := range filter {
|
|
||||||
for _, topic := range rule {
|
|
||||||
for _, bit := range topic {
|
|
||||||
matcher.addScheduler(bit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Track the number of retrieval requests made
|
|
||||||
var requested atomic.Uint32
|
|
||||||
|
|
||||||
// Start the matching session for the filter and the retriever goroutines
|
|
||||||
quit := make(chan struct{})
|
|
||||||
matches := make(chan uint64, 16)
|
|
||||||
|
|
||||||
session, err := matcher.Start(context.Background(), start, blocks-1, matches)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to stat matcher session: %v", err)
|
|
||||||
}
|
|
||||||
startRetrievers(session, quit, &requested, maxReqCount)
|
|
||||||
|
|
||||||
// Iterate over all the blocks and verify that the pipeline produces the correct matches
|
|
||||||
for i := start; i < blocks; i++ {
|
|
||||||
if expMatch3(filter, i) {
|
|
||||||
match, ok := <-matches
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("filter = %v blocks = %v intermittent = %v: expected #%v, results channel closed", filter, blocks, intermittent, i)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if match != i {
|
|
||||||
t.Errorf("filter = %v blocks = %v intermittent = %v: expected #%v, got #%v", filter, blocks, intermittent, i, match)
|
|
||||||
}
|
|
||||||
// If we're testing intermittent mode, abort and restart the pipeline
|
|
||||||
if intermittent {
|
|
||||||
session.Close()
|
|
||||||
close(quit)
|
|
||||||
|
|
||||||
quit = make(chan struct{})
|
|
||||||
matches = make(chan uint64, 16)
|
|
||||||
|
|
||||||
session, err = matcher.Start(context.Background(), i+1, blocks-1, matches)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to stat matcher session: %v", err)
|
|
||||||
}
|
|
||||||
startRetrievers(session, quit, &requested, maxReqCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ensure the result channel is torn down after the last block
|
|
||||||
match, ok := <-matches
|
|
||||||
if ok {
|
|
||||||
t.Errorf("filter = %v blocks = %v intermittent = %v: expected closed channel, got #%v", filter, blocks, intermittent, match)
|
|
||||||
}
|
|
||||||
// Clean up the session and ensure we match the expected retrieval count
|
|
||||||
session.Close()
|
|
||||||
close(quit)
|
|
||||||
|
|
||||||
if retrievals != 0 && requested.Load() != retrievals {
|
|
||||||
t.Errorf("filter = %v blocks = %v intermittent = %v: request count mismatch, have #%v, want #%v", filter, blocks, intermittent, requested.Load(), retrievals)
|
|
||||||
}
|
|
||||||
return requested.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// startRetrievers starts a batch of goroutines listening for section requests
|
|
||||||
// and serving them.
|
|
||||||
func startRetrievers(session *MatcherSession, quit chan struct{}, retrievals *atomic.Uint32, batch int) {
|
|
||||||
requests := make(chan chan *Retrieval)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
// Start a multiplexer to test multiple threaded execution
|
|
||||||
go session.Multiplex(batch, 100*time.Microsecond, requests)
|
|
||||||
|
|
||||||
// Start a services to match the above multiplexer
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// Wait for a service request or a shutdown
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case request := <-requests:
|
|
||||||
task := <-request
|
|
||||||
|
|
||||||
task.Bitsets = make([][]byte, len(task.Sections))
|
|
||||||
for i, section := range task.Sections {
|
|
||||||
if rand.Int()%4 != 0 { // Handle occasional missing deliveries
|
|
||||||
task.Bitsets[i] = generateBitset(task.Bit, section)
|
|
||||||
retrievals.Add(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request <- task
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateBitset generates the rotated bitset for the given bloom bit and section
|
|
||||||
// numbers.
|
|
||||||
func generateBitset(bit uint, section uint64) []byte {
|
|
||||||
bitset := make([]byte, testSectionSize/8)
|
|
||||||
for i := 0; i < len(bitset); i++ {
|
|
||||||
for b := 0; b < 8; b++ {
|
|
||||||
blockIdx := section*testSectionSize + uint64(i*8+b)
|
|
||||||
bitset[i] += bitset[i]
|
|
||||||
if (blockIdx % uint64(bit)) == 0 {
|
|
||||||
bitset[i]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bitset
|
|
||||||
}
|
|
||||||
|
|
||||||
func expMatch1(filter bloomIndexes, i uint64) bool {
|
|
||||||
for _, ii := range filter {
|
|
||||||
if (i % uint64(ii)) != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func expMatch2(filter []bloomIndexes, i uint64) bool {
|
|
||||||
for _, ii := range filter {
|
|
||||||
if expMatch1(ii, i) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func expMatch3(filter [][]bloomIndexes, i uint64) bool {
|
|
||||||
for _, ii := range filter {
|
|
||||||
if !expMatch2(ii, i) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bloombits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// request represents a bloom retrieval task to prioritize and pull from the local
|
|
||||||
// database or remotely from the network.
|
|
||||||
type request struct {
|
|
||||||
section uint64 // Section index to retrieve the a bit-vector from
|
|
||||||
bit uint // Bit index within the section to retrieve the vector of
|
|
||||||
}
|
|
||||||
|
|
||||||
// response represents the state of a requested bit-vector through a scheduler.
|
|
||||||
type response struct {
|
|
||||||
cached []byte // Cached bits to dedup multiple requests
|
|
||||||
done chan struct{} // Channel to allow waiting for completion
|
|
||||||
}
|
|
||||||
|
|
||||||
// scheduler handles the scheduling of bloom-filter retrieval operations for
|
|
||||||
// entire section-batches belonging to a single bloom bit. Beside scheduling the
|
|
||||||
// retrieval operations, this struct also deduplicates the requests and caches
|
|
||||||
// the results to minimize network/database overhead even in complex filtering
|
|
||||||
// scenarios.
|
|
||||||
type scheduler struct {
|
|
||||||
bit uint // Index of the bit in the bloom filter this scheduler is responsible for
|
|
||||||
responses map[uint64]*response // Currently pending retrieval requests or already cached responses
|
|
||||||
lock sync.Mutex // Lock protecting the responses from concurrent access
|
|
||||||
}
|
|
||||||
|
|
||||||
// newScheduler creates a new bloom-filter retrieval scheduler for a specific
|
|
||||||
// bit index.
|
|
||||||
func newScheduler(idx uint) *scheduler {
|
|
||||||
return &scheduler{
|
|
||||||
bit: idx,
|
|
||||||
responses: make(map[uint64]*response),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// run creates a retrieval pipeline, receiving section indexes from sections and
|
|
||||||
// returning the results in the same order through the done channel. Concurrent
|
|
||||||
// runs of the same scheduler are allowed, leading to retrieval task deduplication.
|
|
||||||
func (s *scheduler) run(sections chan uint64, dist chan *request, done chan []byte, quit chan struct{}, wg *sync.WaitGroup) {
|
|
||||||
// Create a forwarder channel between requests and responses of the same size as
|
|
||||||
// the distribution channel (since that will block the pipeline anyway).
|
|
||||||
pend := make(chan uint64, cap(dist))
|
|
||||||
|
|
||||||
// Start the pipeline schedulers to forward between user -> distributor -> user
|
|
||||||
wg.Add(2)
|
|
||||||
go s.scheduleRequests(sections, dist, pend, quit, wg)
|
|
||||||
go s.scheduleDeliveries(pend, done, quit, wg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset cleans up any leftovers from previous runs. This is required before a
|
|
||||||
// restart to ensure the no previously requested but never delivered state will
|
|
||||||
// cause a lockup.
|
|
||||||
func (s *scheduler) reset() {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
for section, res := range s.responses {
|
|
||||||
if res.cached == nil {
|
|
||||||
delete(s.responses, section)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// scheduleRequests reads section retrieval requests from the input channel,
|
|
||||||
// deduplicates the stream and pushes unique retrieval tasks into the distribution
|
|
||||||
// channel for a database or network layer to honour.
|
|
||||||
func (s *scheduler) scheduleRequests(reqs chan uint64, dist chan *request, pend chan uint64, quit chan struct{}, wg *sync.WaitGroup) {
|
|
||||||
// Clean up the goroutine and pipeline when done
|
|
||||||
defer wg.Done()
|
|
||||||
defer close(pend)
|
|
||||||
|
|
||||||
// Keep reading and scheduling section requests
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case section, ok := <-reqs:
|
|
||||||
// New section retrieval requested
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Deduplicate retrieval requests
|
|
||||||
unique := false
|
|
||||||
|
|
||||||
s.lock.Lock()
|
|
||||||
if s.responses[section] == nil {
|
|
||||||
s.responses[section] = &response{
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
unique = true
|
|
||||||
}
|
|
||||||
s.lock.Unlock()
|
|
||||||
|
|
||||||
// Schedule the section for retrieval and notify the deliverer to expect this section
|
|
||||||
if unique {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
case dist <- &request{bit: s.bit, section: section}:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
case pend <- section:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// scheduleDeliveries reads section acceptance notifications and waits for them
|
|
||||||
// to be delivered, pushing them into the output data buffer.
|
|
||||||
func (s *scheduler) scheduleDeliveries(pend chan uint64, done chan []byte, quit chan struct{}, wg *sync.WaitGroup) {
|
|
||||||
// Clean up the goroutine and pipeline when done
|
|
||||||
defer wg.Done()
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
// Keep reading notifications and scheduling deliveries
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case idx, ok := <-pend:
|
|
||||||
// New section retrieval pending
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Wait until the request is honoured
|
|
||||||
s.lock.Lock()
|
|
||||||
res := s.responses[idx]
|
|
||||||
s.lock.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
case <-res.done:
|
|
||||||
}
|
|
||||||
// Deliver the result
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
return
|
|
||||||
case done <- res.cached:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deliver is called by the request distributor when a reply to a request arrives.
|
|
||||||
func (s *scheduler) deliver(sections []uint64, data [][]byte) {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
for i, section := range sections {
|
|
||||||
if res := s.responses[section]; res != nil && res.cached == nil { // Avoid non-requests and double deliveries
|
|
||||||
res.cached = data[i]
|
|
||||||
close(res.done)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package bloombits
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/big"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that the scheduler can deduplicate and forward retrieval requests to
|
|
||||||
// underlying fetchers and serve responses back, irrelevant of the concurrency
|
|
||||||
// of the requesting clients or serving data fetchers.
|
|
||||||
func TestSchedulerSingleClientSingleFetcher(t *testing.T) { testScheduler(t, 1, 1, 5000) }
|
|
||||||
func TestSchedulerSingleClientMultiFetcher(t *testing.T) { testScheduler(t, 1, 10, 5000) }
|
|
||||||
func TestSchedulerMultiClientSingleFetcher(t *testing.T) { testScheduler(t, 10, 1, 5000) }
|
|
||||||
func TestSchedulerMultiClientMultiFetcher(t *testing.T) { testScheduler(t, 10, 10, 5000) }
|
|
||||||
|
|
||||||
func testScheduler(t *testing.T, clients int, fetchers int, requests int) {
|
|
||||||
t.Parallel()
|
|
||||||
f := newScheduler(0)
|
|
||||||
|
|
||||||
// Create a batch of handler goroutines that respond to bloom bit requests and
|
|
||||||
// deliver them to the scheduler.
|
|
||||||
var fetchPend sync.WaitGroup
|
|
||||||
fetchPend.Add(fetchers)
|
|
||||||
defer fetchPend.Wait()
|
|
||||||
|
|
||||||
fetch := make(chan *request, 16)
|
|
||||||
defer close(fetch)
|
|
||||||
|
|
||||||
var delivered atomic.Uint32
|
|
||||||
for i := 0; i < fetchers; i++ {
|
|
||||||
go func() {
|
|
||||||
defer fetchPend.Done()
|
|
||||||
|
|
||||||
for req := range fetch {
|
|
||||||
delivered.Add(1)
|
|
||||||
|
|
||||||
f.deliver([]uint64{
|
|
||||||
req.section + uint64(requests), // Non-requested data (ensure it doesn't go out of bounds)
|
|
||||||
req.section, // Requested data
|
|
||||||
req.section, // Duplicated data (ensure it doesn't double close anything)
|
|
||||||
}, [][]byte{
|
|
||||||
{},
|
|
||||||
new(big.Int).SetUint64(req.section).Bytes(),
|
|
||||||
new(big.Int).SetUint64(req.section).Bytes(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// Start a batch of goroutines to concurrently run scheduling tasks
|
|
||||||
quit := make(chan struct{})
|
|
||||||
|
|
||||||
var pend sync.WaitGroup
|
|
||||||
pend.Add(clients)
|
|
||||||
|
|
||||||
for i := 0; i < clients; i++ {
|
|
||||||
go func() {
|
|
||||||
defer pend.Done()
|
|
||||||
|
|
||||||
in := make(chan uint64, 16)
|
|
||||||
out := make(chan []byte, 16)
|
|
||||||
|
|
||||||
f.run(in, fetch, out, quit, &pend)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for j := 0; j < requests; j++ {
|
|
||||||
in <- uint64(j)
|
|
||||||
}
|
|
||||||
close(in)
|
|
||||||
}()
|
|
||||||
b := new(big.Int)
|
|
||||||
for j := 0; j < requests; j++ {
|
|
||||||
bits := <-out
|
|
||||||
if want := b.SetUint64(uint64(j)).Bytes(); !bytes.Equal(bits, want) {
|
|
||||||
t.Errorf("vector %d: delivered content mismatch: have %x, want %x", j, bits, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
pend.Wait()
|
|
||||||
|
|
||||||
if have := delivered.Load(); int(have) != requests {
|
|
||||||
t.Errorf("request count mismatch: have %v, want %v", have, requests)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,523 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChainIndexerBackend defines the methods needed to process chain segments in
|
|
||||||
// the background and write the segment results into the database. These can be
|
|
||||||
// used to create filter blooms or CHTs.
|
|
||||||
type ChainIndexerBackend interface {
|
|
||||||
// Reset initiates the processing of a new chain segment, potentially terminating
|
|
||||||
// any partially completed operations (in case of a reorg).
|
|
||||||
Reset(ctx context.Context, section uint64, prevHead common.Hash) error
|
|
||||||
|
|
||||||
// Process crunches through the next header in the chain segment. The caller
|
|
||||||
// will ensure a sequential order of headers.
|
|
||||||
Process(ctx context.Context, header *types.Header) error
|
|
||||||
|
|
||||||
// Commit finalizes the section metadata and stores it into the database.
|
|
||||||
Commit() error
|
|
||||||
|
|
||||||
// Prune deletes the chain index older than the given threshold.
|
|
||||||
Prune(threshold uint64) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainIndexerChain interface is used for connecting the indexer to a blockchain
|
|
||||||
type ChainIndexerChain interface {
|
|
||||||
// CurrentHeader retrieves the latest locally known header.
|
|
||||||
CurrentHeader() *types.Header
|
|
||||||
|
|
||||||
// SubscribeChainHeadEvent subscribes to new head header notifications.
|
|
||||||
SubscribeChainHeadEvent(ch chan<- ChainHeadEvent) event.Subscription
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainIndexer does a post-processing job for equally sized sections of the
|
|
||||||
// canonical chain (like BlooomBits and CHT structures). A ChainIndexer is
|
|
||||||
// connected to the blockchain through the event system by starting a
|
|
||||||
// ChainHeadEventLoop in a goroutine.
|
|
||||||
//
|
|
||||||
// Further child ChainIndexers can be added which use the output of the parent
|
|
||||||
// section indexer. These child indexers receive new head notifications only
|
|
||||||
// after an entire section has been finished or in case of rollbacks that might
|
|
||||||
// affect already finished sections.
|
|
||||||
type ChainIndexer struct {
|
|
||||||
chainDb ethdb.Database // Chain database to index the data from
|
|
||||||
indexDb ethdb.Database // Prefixed table-view of the db to write index metadata into
|
|
||||||
backend ChainIndexerBackend // Background processor generating the index data content
|
|
||||||
children []*ChainIndexer // Child indexers to cascade chain updates to
|
|
||||||
|
|
||||||
active atomic.Bool // Flag whether the event loop was started
|
|
||||||
update chan struct{} // Notification channel that headers should be processed
|
|
||||||
quit chan chan error // Quit channel to tear down running goroutines
|
|
||||||
ctx context.Context
|
|
||||||
ctxCancel func()
|
|
||||||
|
|
||||||
sectionSize uint64 // Number of blocks in a single chain segment to process
|
|
||||||
confirmsReq uint64 // Number of confirmations before processing a completed segment
|
|
||||||
|
|
||||||
storedSections uint64 // Number of sections successfully indexed into the database
|
|
||||||
knownSections uint64 // Number of sections known to be complete (block wise)
|
|
||||||
cascadedHead uint64 // Block number of the last completed section cascaded to subindexers
|
|
||||||
|
|
||||||
checkpointSections uint64 // Number of sections covered by the checkpoint
|
|
||||||
checkpointHead common.Hash // Section head belonging to the checkpoint
|
|
||||||
|
|
||||||
throttling time.Duration // Disk throttling to prevent a heavy upgrade from hogging resources
|
|
||||||
|
|
||||||
log log.Logger
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChainIndexer creates a new chain indexer to do background processing on
|
|
||||||
// chain segments of a given size after certain number of confirmations passed.
|
|
||||||
// The throttling parameter might be used to prevent database thrashing.
|
|
||||||
func NewChainIndexer(chainDb ethdb.Database, indexDb ethdb.Database, backend ChainIndexerBackend, section, confirm uint64, throttling time.Duration, kind string) *ChainIndexer {
|
|
||||||
c := &ChainIndexer{
|
|
||||||
chainDb: chainDb,
|
|
||||||
indexDb: indexDb,
|
|
||||||
backend: backend,
|
|
||||||
update: make(chan struct{}, 1),
|
|
||||||
quit: make(chan chan error),
|
|
||||||
sectionSize: section,
|
|
||||||
confirmsReq: confirm,
|
|
||||||
throttling: throttling,
|
|
||||||
log: log.New("type", kind),
|
|
||||||
}
|
|
||||||
// Initialize database dependent fields and start the updater
|
|
||||||
c.loadValidSections()
|
|
||||||
c.ctx, c.ctxCancel = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
go c.updateLoop()
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddCheckpoint adds a checkpoint. Sections are never processed and the chain
|
|
||||||
// is not expected to be available before this point. The indexer assumes that
|
|
||||||
// the backend has sufficient information available to process subsequent sections.
|
|
||||||
//
|
|
||||||
// Note: knownSections == 0 and storedSections == checkpointSections until
|
|
||||||
// syncing reaches the checkpoint
|
|
||||||
func (c *ChainIndexer) AddCheckpoint(section uint64, shead common.Hash) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// Short circuit if the given checkpoint is below than local's.
|
|
||||||
if c.checkpointSections >= section+1 || section < c.storedSections {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.checkpointSections = section + 1
|
|
||||||
c.checkpointHead = shead
|
|
||||||
|
|
||||||
c.setSectionHead(section, shead)
|
|
||||||
c.setValidSections(section + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start creates a goroutine to feed chain head events into the indexer for
|
|
||||||
// cascading background processing. Children do not need to be started, they
|
|
||||||
// are notified about new events by their parents.
|
|
||||||
func (c *ChainIndexer) Start(chain ChainIndexerChain) {
|
|
||||||
events := make(chan ChainHeadEvent, 10)
|
|
||||||
sub := chain.SubscribeChainHeadEvent(events)
|
|
||||||
|
|
||||||
go c.eventLoop(chain.CurrentHeader(), events, sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close tears down all goroutines belonging to the indexer and returns any error
|
|
||||||
// that might have occurred internally.
|
|
||||||
func (c *ChainIndexer) Close() error {
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
c.ctxCancel()
|
|
||||||
|
|
||||||
// Tear down the primary update loop
|
|
||||||
errc := make(chan error)
|
|
||||||
c.quit <- errc
|
|
||||||
if err := <-errc; err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
// If needed, tear down the secondary event loop
|
|
||||||
if c.active.Load() {
|
|
||||||
c.quit <- errc
|
|
||||||
if err := <-errc; err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Close all children
|
|
||||||
for _, child := range c.children {
|
|
||||||
if err := child.Close(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return any failures
|
|
||||||
switch {
|
|
||||||
case len(errs) == 0:
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case len(errs) == 1:
|
|
||||||
return errs[0]
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventLoop is a secondary - optional - event loop of the indexer which is only
|
|
||||||
// started for the outermost indexer to push chain head events into a processing
|
|
||||||
// queue.
|
|
||||||
func (c *ChainIndexer) eventLoop(currentHeader *types.Header, events chan ChainHeadEvent, sub event.Subscription) {
|
|
||||||
// Mark the chain indexer as active, requiring an additional teardown
|
|
||||||
c.active.Store(true)
|
|
||||||
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// Fire the initial new head event to start any outstanding processing
|
|
||||||
c.newHead(currentHeader.Number.Uint64(), false)
|
|
||||||
|
|
||||||
var (
|
|
||||||
prevHeader = currentHeader
|
|
||||||
prevHash = currentHeader.Hash()
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case errc := <-c.quit:
|
|
||||||
// Chain indexer terminating, report no failure and abort
|
|
||||||
errc <- nil
|
|
||||||
return
|
|
||||||
|
|
||||||
case ev, ok := <-events:
|
|
||||||
// Received a new event, ensure it's not nil (closing) and update
|
|
||||||
if !ok {
|
|
||||||
errc := <-c.quit
|
|
||||||
errc <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
header := ev.Block.Header()
|
|
||||||
if header.ParentHash != prevHash {
|
|
||||||
// Reorg to the common ancestor if needed (might not exist in light sync mode, skip reorg then)
|
|
||||||
// TODO(karalabe, zsfelfoldi): This seems a bit brittle, can we detect this case explicitly?
|
|
||||||
|
|
||||||
if rawdb.ReadCanonicalHash(c.chainDb, prevHeader.Number.Uint64()) != prevHash {
|
|
||||||
if h := rawdb.FindCommonAncestor(c.chainDb, prevHeader, header); h != nil {
|
|
||||||
c.newHead(h.Number.Uint64(), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.newHead(header.Number.Uint64(), false)
|
|
||||||
|
|
||||||
prevHeader, prevHash = header, header.Hash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// newHead notifies the indexer about new chain heads and/or reorgs.
|
|
||||||
func (c *ChainIndexer) newHead(head uint64, reorg bool) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
// If a reorg happened, invalidate all sections until that point
|
|
||||||
if reorg {
|
|
||||||
// Revert the known section number to the reorg point
|
|
||||||
known := (head + 1) / c.sectionSize
|
|
||||||
stored := known
|
|
||||||
if known < c.checkpointSections {
|
|
||||||
known = 0
|
|
||||||
}
|
|
||||||
if stored < c.checkpointSections {
|
|
||||||
stored = c.checkpointSections
|
|
||||||
}
|
|
||||||
if known < c.knownSections {
|
|
||||||
c.knownSections = known
|
|
||||||
}
|
|
||||||
// Revert the stored sections from the database to the reorg point
|
|
||||||
if stored < c.storedSections {
|
|
||||||
c.setValidSections(stored)
|
|
||||||
}
|
|
||||||
// Update the new head number to the finalized section end and notify children
|
|
||||||
head = known * c.sectionSize
|
|
||||||
|
|
||||||
if head < c.cascadedHead {
|
|
||||||
c.cascadedHead = head
|
|
||||||
for _, child := range c.children {
|
|
||||||
child.newHead(c.cascadedHead, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// No reorg, calculate the number of newly known sections and update if high enough
|
|
||||||
var sections uint64
|
|
||||||
if head >= c.confirmsReq {
|
|
||||||
sections = (head + 1 - c.confirmsReq) / c.sectionSize
|
|
||||||
if sections < c.checkpointSections {
|
|
||||||
sections = 0
|
|
||||||
}
|
|
||||||
if sections > c.knownSections {
|
|
||||||
if c.knownSections < c.checkpointSections {
|
|
||||||
// syncing reached the checkpoint, verify section head
|
|
||||||
syncedHead := rawdb.ReadCanonicalHash(c.chainDb, c.checkpointSections*c.sectionSize-1)
|
|
||||||
if syncedHead != c.checkpointHead {
|
|
||||||
c.log.Error("Synced chain does not match checkpoint", "number", c.checkpointSections*c.sectionSize-1, "expected", c.checkpointHead, "synced", syncedHead)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.knownSections = sections
|
|
||||||
|
|
||||||
select {
|
|
||||||
case c.update <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateLoop is the main event loop of the indexer which pushes chain segments
|
|
||||||
// down into the processing backend.
|
|
||||||
func (c *ChainIndexer) updateLoop() {
|
|
||||||
var (
|
|
||||||
updating bool
|
|
||||||
updated time.Time
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case errc := <-c.quit:
|
|
||||||
// Chain indexer terminating, report no failure and abort
|
|
||||||
errc <- nil
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-c.update:
|
|
||||||
// Section headers completed (or rolled back), update the index
|
|
||||||
c.lock.Lock()
|
|
||||||
if c.knownSections > c.storedSections {
|
|
||||||
// Periodically print an upgrade log message to the user
|
|
||||||
if time.Since(updated) > 8*time.Second {
|
|
||||||
if c.knownSections > c.storedSections+1 {
|
|
||||||
updating = true
|
|
||||||
c.log.Info("Upgrading chain index", "percentage", c.storedSections*100/c.knownSections)
|
|
||||||
}
|
|
||||||
updated = time.Now()
|
|
||||||
}
|
|
||||||
// Cache the current section count and head to allow unlocking the mutex
|
|
||||||
c.verifyLastHead()
|
|
||||||
section := c.storedSections
|
|
||||||
var oldHead common.Hash
|
|
||||||
if section > 0 {
|
|
||||||
oldHead = c.SectionHead(section - 1)
|
|
||||||
}
|
|
||||||
// Process the newly defined section in the background
|
|
||||||
c.lock.Unlock()
|
|
||||||
newHead, err := c.processSection(section, oldHead)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-c.ctx.Done():
|
|
||||||
<-c.quit <- nil
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
c.log.Error("Section processing failed", "error", err)
|
|
||||||
}
|
|
||||||
c.lock.Lock()
|
|
||||||
|
|
||||||
// If processing succeeded and no reorgs occurred, mark the section completed
|
|
||||||
if err == nil && (section == 0 || oldHead == c.SectionHead(section-1)) {
|
|
||||||
c.setSectionHead(section, newHead)
|
|
||||||
c.setValidSections(section + 1)
|
|
||||||
if c.storedSections == c.knownSections && updating {
|
|
||||||
updating = false
|
|
||||||
c.log.Info("Finished upgrading chain index")
|
|
||||||
}
|
|
||||||
c.cascadedHead = c.storedSections*c.sectionSize - 1
|
|
||||||
for _, child := range c.children {
|
|
||||||
c.log.Trace("Cascading chain index update", "head", c.cascadedHead)
|
|
||||||
child.newHead(c.cascadedHead, false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If processing failed, don't retry until further notification
|
|
||||||
c.log.Debug("Chain index processing failed", "section", section, "err", err)
|
|
||||||
c.verifyLastHead()
|
|
||||||
c.knownSections = c.storedSections
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If there are still further sections to process, reschedule
|
|
||||||
if c.knownSections > c.storedSections {
|
|
||||||
time.AfterFunc(c.throttling, func() {
|
|
||||||
select {
|
|
||||||
case c.update <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
c.lock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// processSection processes an entire section by calling backend functions while
|
|
||||||
// ensuring the continuity of the passed headers. Since the chain mutex is not
|
|
||||||
// held while processing, the continuity can be broken by a long reorg, in which
|
|
||||||
// case the function returns with an error.
|
|
||||||
func (c *ChainIndexer) processSection(section uint64, lastHead common.Hash) (common.Hash, error) {
|
|
||||||
c.log.Trace("Processing new chain section", "section", section)
|
|
||||||
|
|
||||||
// Reset and partial processing
|
|
||||||
if err := c.backend.Reset(c.ctx, section, lastHead); err != nil {
|
|
||||||
c.setValidSections(0)
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for number := section * c.sectionSize; number < (section+1)*c.sectionSize; number++ {
|
|
||||||
hash := rawdb.ReadCanonicalHash(c.chainDb, number)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
return common.Hash{}, fmt.Errorf("canonical block #%d unknown", number)
|
|
||||||
}
|
|
||||||
header := rawdb.ReadHeader(c.chainDb, hash, number)
|
|
||||||
if header == nil {
|
|
||||||
return common.Hash{}, fmt.Errorf("block #%d [%x..] not found", number, hash[:4])
|
|
||||||
} else if header.ParentHash != lastHead {
|
|
||||||
return common.Hash{}, errors.New("chain reorged during section processing")
|
|
||||||
}
|
|
||||||
if err := c.backend.Process(c.ctx, header); err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
lastHead = header.Hash()
|
|
||||||
}
|
|
||||||
if err := c.backend.Commit(); err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
return lastHead, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyLastHead compares last stored section head with the corresponding block hash in the
|
|
||||||
// actual canonical chain and rolls back reorged sections if necessary to ensure that stored
|
|
||||||
// sections are all valid
|
|
||||||
func (c *ChainIndexer) verifyLastHead() {
|
|
||||||
for c.storedSections > 0 && c.storedSections > c.checkpointSections {
|
|
||||||
if c.SectionHead(c.storedSections-1) == rawdb.ReadCanonicalHash(c.chainDb, c.storedSections*c.sectionSize-1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.setValidSections(c.storedSections - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sections returns the number of processed sections maintained by the indexer
|
|
||||||
// and also the information about the last header indexed for potential canonical
|
|
||||||
// verifications.
|
|
||||||
func (c *ChainIndexer) Sections() (uint64, uint64, common.Hash) {
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
c.verifyLastHead()
|
|
||||||
return c.storedSections, c.storedSections*c.sectionSize - 1, c.SectionHead(c.storedSections - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddChildIndexer adds a child ChainIndexer that can use the output of this one
|
|
||||||
func (c *ChainIndexer) AddChildIndexer(indexer *ChainIndexer) {
|
|
||||||
if indexer == c {
|
|
||||||
panic("can't add indexer as a child of itself")
|
|
||||||
}
|
|
||||||
c.lock.Lock()
|
|
||||||
defer c.lock.Unlock()
|
|
||||||
|
|
||||||
c.children = append(c.children, indexer)
|
|
||||||
|
|
||||||
// Cascade any pending updates to new children too
|
|
||||||
sections := c.storedSections
|
|
||||||
if c.knownSections < sections {
|
|
||||||
// if a section is "stored" but not "known" then it is a checkpoint without
|
|
||||||
// available chain data so we should not cascade it yet
|
|
||||||
sections = c.knownSections
|
|
||||||
}
|
|
||||||
if sections > 0 {
|
|
||||||
indexer.newHead(sections*c.sectionSize-1, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune deletes all chain data older than given threshold.
|
|
||||||
func (c *ChainIndexer) Prune(threshold uint64) error {
|
|
||||||
return c.backend.Prune(threshold)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadValidSections reads the number of valid sections from the index database
|
|
||||||
// and caches is into the local state.
|
|
||||||
func (c *ChainIndexer) loadValidSections() {
|
|
||||||
data, _ := c.indexDb.Get([]byte("count"))
|
|
||||||
if len(data) == 8 {
|
|
||||||
c.storedSections = binary.BigEndian.Uint64(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setValidSections writes the number of valid sections to the index database
|
|
||||||
func (c *ChainIndexer) setValidSections(sections uint64) {
|
|
||||||
// Set the current number of valid sections in the database
|
|
||||||
var data [8]byte
|
|
||||||
binary.BigEndian.PutUint64(data[:], sections)
|
|
||||||
c.indexDb.Put([]byte("count"), data[:])
|
|
||||||
|
|
||||||
// Remove any reorged sections, caching the valids in the mean time
|
|
||||||
for c.storedSections > sections {
|
|
||||||
c.storedSections--
|
|
||||||
c.removeSectionHead(c.storedSections)
|
|
||||||
}
|
|
||||||
c.storedSections = sections // needed if new > old
|
|
||||||
}
|
|
||||||
|
|
||||||
// SectionHead retrieves the last block hash of a processed section from the
|
|
||||||
// index database.
|
|
||||||
func (c *ChainIndexer) SectionHead(section uint64) common.Hash {
|
|
||||||
var data [8]byte
|
|
||||||
binary.BigEndian.PutUint64(data[:], section)
|
|
||||||
|
|
||||||
hash, _ := c.indexDb.Get(append([]byte("shead"), data[:]...))
|
|
||||||
if len(hash) == len(common.Hash{}) {
|
|
||||||
return common.BytesToHash(hash)
|
|
||||||
}
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setSectionHead writes the last block hash of a processed section to the index
|
|
||||||
// database.
|
|
||||||
func (c *ChainIndexer) setSectionHead(section uint64, hash common.Hash) {
|
|
||||||
var data [8]byte
|
|
||||||
binary.BigEndian.PutUint64(data[:], section)
|
|
||||||
|
|
||||||
c.indexDb.Put(append([]byte("shead"), data[:]...), hash.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeSectionHead removes the reference to a processed section from the index
|
|
||||||
// database.
|
|
||||||
func (c *ChainIndexer) removeSectionHead(section uint64) {
|
|
||||||
var data [8]byte
|
|
||||||
binary.BigEndian.PutUint64(data[:], section)
|
|
||||||
|
|
||||||
c.indexDb.Delete(append([]byte("shead"), data[:]...))
|
|
||||||
}
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Runs multiple tests with randomized parameters.
|
|
||||||
func TestChainIndexerSingle(t *testing.T) {
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
testChainIndexer(t, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs multiple tests with randomized parameters and different number of
|
|
||||||
// chain backends.
|
|
||||||
func TestChainIndexerWithChildren(t *testing.T) {
|
|
||||||
for i := 2; i < 8; i++ {
|
|
||||||
testChainIndexer(t, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testChainIndexer runs a test with either a single chain indexer or a chain of
|
|
||||||
// multiple backends. The section size and required confirmation count parameters
|
|
||||||
// are randomized.
|
|
||||||
func testChainIndexer(t *testing.T, count int) {
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Create a chain of indexers and ensure they all report empty
|
|
||||||
backends := make([]*testChainIndexBackend, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
var (
|
|
||||||
sectionSize = uint64(rand.Intn(100) + 1)
|
|
||||||
confirmsReq = uint64(rand.Intn(10))
|
|
||||||
)
|
|
||||||
backends[i] = &testChainIndexBackend{t: t, processCh: make(chan uint64)}
|
|
||||||
backends[i].indexer = NewChainIndexer(db, rawdb.NewTable(db, string([]byte{byte(i)})), backends[i], sectionSize, confirmsReq, 0, fmt.Sprintf("indexer-%d", i))
|
|
||||||
|
|
||||||
if sections, _, _ := backends[i].indexer.Sections(); sections != 0 {
|
|
||||||
t.Fatalf("Canonical section count mismatch: have %v, want %v", sections, 0)
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
backends[i-1].indexer.AddChildIndexer(backends[i].indexer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer backends[0].indexer.Close() // parent indexer shuts down children
|
|
||||||
// notify pings the root indexer about a new head or reorg, then expect
|
|
||||||
// processed blocks if a section is processable
|
|
||||||
notify := func(headNum, failNum uint64, reorg bool) {
|
|
||||||
backends[0].indexer.newHead(headNum, reorg)
|
|
||||||
if reorg {
|
|
||||||
for _, backend := range backends {
|
|
||||||
headNum = backend.reorg(headNum)
|
|
||||||
backend.assertSections()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var cascade bool
|
|
||||||
for _, backend := range backends {
|
|
||||||
headNum, cascade = backend.assertBlocks(headNum, failNum)
|
|
||||||
if !cascade {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
backend.assertSections()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// inject inserts a new random canonical header into the database directly
|
|
||||||
inject := func(number uint64) {
|
|
||||||
header := &types.Header{Number: big.NewInt(int64(number)), Extra: big.NewInt(rand.Int63()).Bytes()}
|
|
||||||
if number > 0 {
|
|
||||||
header.ParentHash = rawdb.ReadCanonicalHash(db, number-1)
|
|
||||||
}
|
|
||||||
rawdb.WriteHeader(db, header)
|
|
||||||
rawdb.WriteCanonicalHash(db, header.Hash(), number)
|
|
||||||
}
|
|
||||||
// Start indexer with an already existing chain
|
|
||||||
for i := uint64(0); i <= 100; i++ {
|
|
||||||
inject(i)
|
|
||||||
}
|
|
||||||
notify(100, 100, false)
|
|
||||||
|
|
||||||
// Add new blocks one by one
|
|
||||||
for i := uint64(101); i <= 1000; i++ {
|
|
||||||
inject(i)
|
|
||||||
notify(i, i, false)
|
|
||||||
}
|
|
||||||
// Do a reorg
|
|
||||||
notify(500, 500, true)
|
|
||||||
|
|
||||||
// Create new fork
|
|
||||||
for i := uint64(501); i <= 1000; i++ {
|
|
||||||
inject(i)
|
|
||||||
notify(i, i, false)
|
|
||||||
}
|
|
||||||
for i := uint64(1001); i <= 1500; i++ {
|
|
||||||
inject(i)
|
|
||||||
}
|
|
||||||
// Failed processing scenario where less blocks are available than notified
|
|
||||||
notify(2000, 1500, false)
|
|
||||||
|
|
||||||
// Notify about a reorg (which could have caused the missing blocks if happened during processing)
|
|
||||||
notify(1500, 1500, true)
|
|
||||||
|
|
||||||
// Create new fork
|
|
||||||
for i := uint64(1501); i <= 2000; i++ {
|
|
||||||
inject(i)
|
|
||||||
notify(i, i, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testChainIndexBackend implements ChainIndexerBackend
|
|
||||||
type testChainIndexBackend struct {
|
|
||||||
t *testing.T
|
|
||||||
indexer *ChainIndexer
|
|
||||||
section, headerCnt, stored uint64
|
|
||||||
processCh chan uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// assertSections verifies if a chain indexer has the correct number of section.
|
|
||||||
func (b *testChainIndexBackend) assertSections() {
|
|
||||||
// Keep trying for 3 seconds if it does not match
|
|
||||||
var sections uint64
|
|
||||||
for i := 0; i < 300; i++ {
|
|
||||||
sections, _, _ = b.indexer.Sections()
|
|
||||||
if sections == b.stored {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
b.t.Fatalf("Canonical section count mismatch: have %v, want %v", sections, b.stored)
|
|
||||||
}
|
|
||||||
|
|
||||||
// assertBlocks expects processing calls after new blocks have arrived. If the
|
|
||||||
// failNum < headNum then we are simulating a scenario where a reorg has happened
|
|
||||||
// after the processing has started and the processing of a section fails.
|
|
||||||
func (b *testChainIndexBackend) assertBlocks(headNum, failNum uint64) (uint64, bool) {
|
|
||||||
var sections uint64
|
|
||||||
if headNum >= b.indexer.confirmsReq {
|
|
||||||
sections = (headNum + 1 - b.indexer.confirmsReq) / b.indexer.sectionSize
|
|
||||||
if sections > b.stored {
|
|
||||||
// expect processed blocks
|
|
||||||
for expectd := b.stored * b.indexer.sectionSize; expectd < sections*b.indexer.sectionSize; expectd++ {
|
|
||||||
if expectd > failNum {
|
|
||||||
// rolled back after processing started, no more process calls expected
|
|
||||||
// wait until updating is done to make sure that processing actually fails
|
|
||||||
var updating bool
|
|
||||||
for i := 0; i < 300; i++ {
|
|
||||||
b.indexer.lock.Lock()
|
|
||||||
updating = b.indexer.knownSections > b.indexer.storedSections
|
|
||||||
b.indexer.lock.Unlock()
|
|
||||||
if !updating {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if updating {
|
|
||||||
b.t.Fatalf("update did not finish")
|
|
||||||
}
|
|
||||||
sections = expectd / b.indexer.sectionSize
|
|
||||||
break
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
b.t.Fatalf("Expected processed block #%d, got nothing", expectd)
|
|
||||||
case processed := <-b.processCh:
|
|
||||||
if processed != expectd {
|
|
||||||
b.t.Errorf("Expected processed block #%d, got #%d", expectd, processed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.stored = sections
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if b.stored == 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return b.stored*b.indexer.sectionSize - 1, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *testChainIndexBackend) reorg(headNum uint64) uint64 {
|
|
||||||
firstChanged := (headNum + 1) / b.indexer.sectionSize
|
|
||||||
if firstChanged < b.stored {
|
|
||||||
b.stored = firstChanged
|
|
||||||
}
|
|
||||||
return b.stored * b.indexer.sectionSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *testChainIndexBackend) Reset(ctx context.Context, section uint64, prevHead common.Hash) error {
|
|
||||||
b.section = section
|
|
||||||
b.headerCnt = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *testChainIndexBackend) Process(ctx context.Context, header *types.Header) error {
|
|
||||||
b.headerCnt++
|
|
||||||
if b.headerCnt > b.indexer.sectionSize {
|
|
||||||
b.t.Error("Processing too many headers")
|
|
||||||
}
|
|
||||||
//t.processCh <- header.Number.Uint64()
|
|
||||||
select {
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
b.t.Error("Unexpected call to Process")
|
|
||||||
// Can't use Fatal since this is not the test's goroutine.
|
|
||||||
// Returning error stops the chainIndexer's updateLoop
|
|
||||||
return errors.New("Unexpected call to Process")
|
|
||||||
case b.processCh <- header.Number.Uint64():
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *testChainIndexBackend) Commit() error {
|
|
||||||
if b.headerCnt != b.indexer.sectionSize {
|
|
||||||
b.t.Error("Not enough headers processed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *testChainIndexBackend) Prune(threshold uint64) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,573 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/misc"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
|
|
||||||
"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"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlockGen creates blocks for testing.
|
|
||||||
// See GenerateChain for a detailed explanation.
|
|
||||||
type BlockGen struct {
|
|
||||||
i int
|
|
||||||
cm *chainMaker
|
|
||||||
parent *types.Block
|
|
||||||
header *types.Header
|
|
||||||
statedb *state.StateDB
|
|
||||||
|
|
||||||
gasPool *GasPool
|
|
||||||
txs []*types.Transaction
|
|
||||||
receipts []*types.Receipt
|
|
||||||
uncles []*types.Header
|
|
||||||
withdrawals []*types.Withdrawal
|
|
||||||
|
|
||||||
engine consensus.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCoinbase sets the coinbase of the generated block.
|
|
||||||
// It can be called at most once.
|
|
||||||
func (b *BlockGen) SetCoinbase(addr common.Address) {
|
|
||||||
if b.gasPool != nil {
|
|
||||||
if len(b.txs) > 0 {
|
|
||||||
panic("coinbase must be set before adding transactions")
|
|
||||||
}
|
|
||||||
panic("coinbase can only be set once")
|
|
||||||
}
|
|
||||||
b.header.Coinbase = addr
|
|
||||||
b.gasPool = new(GasPool).AddGas(b.header.GasLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetExtra sets the extra data field of the generated block.
|
|
||||||
func (b *BlockGen) SetExtra(data []byte) {
|
|
||||||
b.header.Extra = data
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNonce sets the nonce field of the generated block.
|
|
||||||
func (b *BlockGen) SetNonce(nonce types.BlockNonce) {
|
|
||||||
b.header.Nonce = nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDifficulty sets the difficulty field of the generated block. This method is
|
|
||||||
// useful for Clique tests where the difficulty does not depend on time. For the
|
|
||||||
// ethash tests, please use OffsetTime, which implicitly recalculates the diff.
|
|
||||||
func (b *BlockGen) SetDifficulty(diff *big.Int) {
|
|
||||||
b.header.Difficulty = diff
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPos makes the header a PoS-header (0 difficulty)
|
|
||||||
func (b *BlockGen) SetPoS() {
|
|
||||||
b.header.Difficulty = new(big.Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Difficulty returns the currently calculated difficulty of the block.
|
|
||||||
func (b *BlockGen) Difficulty() *big.Int {
|
|
||||||
return new(big.Int).Set(b.header.Difficulty)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetParentBeaconRoot sets the parent beacon root field of the generated
|
|
||||||
// block.
|
|
||||||
func (b *BlockGen) SetParentBeaconRoot(root common.Hash) {
|
|
||||||
b.header.ParentBeaconRoot = &root
|
|
||||||
var (
|
|
||||||
blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase)
|
|
||||||
vmenv = vm.NewEVM(blockContext, vm.TxContext{}, b.statedb, b.cm.config, vm.Config{})
|
|
||||||
)
|
|
||||||
ProcessBeaconBlockRoot(root, vmenv, b.statedb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addTx adds a transaction to the generated block. If no coinbase has
|
|
||||||
// been set, the block's coinbase is set to the zero address.
|
|
||||||
//
|
|
||||||
// There are a few options can be passed as well in order to run some
|
|
||||||
// customized rules.
|
|
||||||
// - bc: enables the ability to query historical block hashes for BLOCKHASH
|
|
||||||
// - vmConfig: extends the flexibility for customizing evm rules, e.g. enable extra EIPs
|
|
||||||
func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transaction) {
|
|
||||||
if b.gasPool == nil {
|
|
||||||
b.SetCoinbase(common.Address{})
|
|
||||||
}
|
|
||||||
b.statedb.SetTxContext(tx.Hash(), len(b.txs))
|
|
||||||
receipt, err := ApplyTransaction(b.cm.config, bc, &b.header.Coinbase, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed, vmConfig)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
b.txs = append(b.txs, tx)
|
|
||||||
b.receipts = append(b.receipts, receipt)
|
|
||||||
if b.header.BlobGasUsed != nil {
|
|
||||||
*b.header.BlobGasUsed += receipt.BlobGasUsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTx adds a transaction to the generated block. If no coinbase has
|
|
||||||
// been set, the block's coinbase is set to the zero address.
|
|
||||||
//
|
|
||||||
// AddTx panics if the transaction cannot be executed. In addition to the protocol-imposed
|
|
||||||
// limitations (gas limit, etc.), there are some further limitations on the content of
|
|
||||||
// transactions that can be added. Notably, contract code relying on the BLOCKHASH
|
|
||||||
// instruction will panic during execution if it attempts to access a block number outside
|
|
||||||
// of the range created by GenerateChain.
|
|
||||||
func (b *BlockGen) AddTx(tx *types.Transaction) {
|
|
||||||
b.addTx(nil, vm.Config{}, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTxWithChain adds a transaction to the generated block. If no coinbase has
|
|
||||||
// been set, the block's coinbase is set to the zero address.
|
|
||||||
//
|
|
||||||
// AddTxWithChain panics if the transaction cannot be executed. In addition to the
|
|
||||||
// protocol-imposed limitations (gas limit, etc.), there are some further limitations on
|
|
||||||
// the content of transactions that can be added. If contract code relies on the BLOCKHASH
|
|
||||||
// instruction, the block in chain will be returned.
|
|
||||||
func (b *BlockGen) AddTxWithChain(bc *BlockChain, tx *types.Transaction) {
|
|
||||||
b.addTx(bc, vm.Config{}, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTxWithVMConfig adds a transaction to the generated block. If no coinbase has
|
|
||||||
// been set, the block's coinbase is set to the zero address.
|
|
||||||
// The evm interpreter can be customized with the provided vm config.
|
|
||||||
func (b *BlockGen) AddTxWithVMConfig(tx *types.Transaction, config vm.Config) {
|
|
||||||
b.addTx(nil, config, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBalance returns the balance of the given address at the generated block.
|
|
||||||
func (b *BlockGen) GetBalance(addr common.Address) *big.Int {
|
|
||||||
return b.statedb.GetBalance(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddUncheckedTx forcefully adds a transaction to the block without any validation.
|
|
||||||
//
|
|
||||||
// AddUncheckedTx will cause consensus failures when used during real
|
|
||||||
// chain processing. This is best used in conjunction with raw block insertion.
|
|
||||||
func (b *BlockGen) AddUncheckedTx(tx *types.Transaction) {
|
|
||||||
b.txs = append(b.txs, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number returns the block number of the block being generated.
|
|
||||||
func (b *BlockGen) Number() *big.Int {
|
|
||||||
return new(big.Int).Set(b.header.Number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timestamp returns the timestamp of the block being generated.
|
|
||||||
func (b *BlockGen) Timestamp() uint64 {
|
|
||||||
return b.header.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// BaseFee returns the EIP-1559 base fee of the block being generated.
|
|
||||||
func (b *BlockGen) BaseFee() *big.Int {
|
|
||||||
return new(big.Int).Set(b.header.BaseFee)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gas returns the amount of gas left in the current block.
|
|
||||||
func (b *BlockGen) Gas() uint64 {
|
|
||||||
return b.header.GasLimit - b.header.GasUsed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signer returns a valid signer instance for the current block.
|
|
||||||
func (b *BlockGen) Signer() types.Signer {
|
|
||||||
return types.MakeSigner(b.cm.config, b.header.Number, b.header.Time)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddUncheckedReceipt forcefully adds a receipts to the block without a
|
|
||||||
// backing transaction.
|
|
||||||
//
|
|
||||||
// AddUncheckedReceipt will cause consensus failures when used during real
|
|
||||||
// chain processing. This is best used in conjunction with raw block insertion.
|
|
||||||
func (b *BlockGen) AddUncheckedReceipt(receipt *types.Receipt) {
|
|
||||||
b.receipts = append(b.receipts, receipt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxNonce returns the next valid transaction nonce for the
|
|
||||||
// account at addr. It panics if the account does not exist.
|
|
||||||
func (b *BlockGen) TxNonce(addr common.Address) uint64 {
|
|
||||||
if !b.statedb.Exist(addr) {
|
|
||||||
panic("account does not exist")
|
|
||||||
}
|
|
||||||
return b.statedb.GetNonce(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddUncle adds an uncle header to the generated block.
|
|
||||||
func (b *BlockGen) AddUncle(h *types.Header) {
|
|
||||||
// The uncle will have the same timestamp and auto-generated difficulty
|
|
||||||
h.Time = b.header.Time
|
|
||||||
|
|
||||||
var parent *types.Header
|
|
||||||
for i := b.i - 1; i >= 0; i-- {
|
|
||||||
if b.cm.chain[i].Hash() == h.ParentHash {
|
|
||||||
parent = b.cm.chain[i].Header()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.Difficulty = b.engine.CalcDifficulty(b.cm, b.header.Time, parent)
|
|
||||||
|
|
||||||
// The gas limit and price should be derived from the parent
|
|
||||||
h.GasLimit = parent.GasLimit
|
|
||||||
if b.cm.config.IsLondon(h.Number) {
|
|
||||||
h.BaseFee = eip1559.CalcBaseFee(b.cm.config, parent)
|
|
||||||
if !b.cm.config.IsLondon(parent.Number) {
|
|
||||||
parentGasLimit := parent.GasLimit * b.cm.config.ElasticityMultiplier()
|
|
||||||
h.GasLimit = CalcGasLimit(parentGasLimit, parentGasLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.uncles = append(b.uncles, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddWithdrawal adds a withdrawal to the generated block.
|
|
||||||
// It returns the withdrawal index.
|
|
||||||
func (b *BlockGen) AddWithdrawal(w *types.Withdrawal) uint64 {
|
|
||||||
cpy := *w
|
|
||||||
cpy.Index = b.nextWithdrawalIndex()
|
|
||||||
b.withdrawals = append(b.withdrawals, &cpy)
|
|
||||||
return cpy.Index
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextWithdrawalIndex computes the index of the next withdrawal.
|
|
||||||
func (b *BlockGen) nextWithdrawalIndex() uint64 {
|
|
||||||
if len(b.withdrawals) != 0 {
|
|
||||||
return b.withdrawals[len(b.withdrawals)-1].Index + 1
|
|
||||||
}
|
|
||||||
for i := b.i - 1; i >= 0; i-- {
|
|
||||||
if wd := b.cm.chain[i].Withdrawals(); len(wd) != 0 {
|
|
||||||
return wd[len(wd)-1].Index + 1
|
|
||||||
}
|
|
||||||
if i == 0 {
|
|
||||||
// Correctly set the index if no parent had withdrawals.
|
|
||||||
if wd := b.cm.bottom.Withdrawals(); len(wd) != 0 {
|
|
||||||
return wd[len(wd)-1].Index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrevBlock returns a previously generated block by number. It panics if
|
|
||||||
// num is greater or equal to the number of the block being generated.
|
|
||||||
// For index -1, PrevBlock returns the parent block given to GenerateChain.
|
|
||||||
func (b *BlockGen) PrevBlock(index int) *types.Block {
|
|
||||||
if index >= b.i {
|
|
||||||
panic(fmt.Errorf("block index %d out of range (%d,%d)", index, -1, b.i))
|
|
||||||
}
|
|
||||||
if index == -1 {
|
|
||||||
return b.cm.bottom
|
|
||||||
}
|
|
||||||
return b.cm.chain[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// OffsetTime modifies the time instance of a block, implicitly changing its
|
|
||||||
// associated difficulty. It's useful to test scenarios where forking is not
|
|
||||||
// tied to chain length directly.
|
|
||||||
func (b *BlockGen) OffsetTime(seconds int64) {
|
|
||||||
b.header.Time += uint64(seconds)
|
|
||||||
if b.header.Time <= b.cm.bottom.Header().Time {
|
|
||||||
panic("block time out of range")
|
|
||||||
}
|
|
||||||
b.header.Difficulty = b.engine.CalcDifficulty(b.cm, b.header.Time, b.parent.Header())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateChain creates a chain of n blocks. The first block's
|
|
||||||
// parent will be the provided parent. db is used to store
|
|
||||||
// intermediate states and should contain the parent's state trie.
|
|
||||||
//
|
|
||||||
// The generator function is called with a new block generator for
|
|
||||||
// every block. Any transactions and uncles added to the generator
|
|
||||||
// become part of the block. If gen is nil, the blocks will be empty
|
|
||||||
// and their coinbase will be the zero address.
|
|
||||||
//
|
|
||||||
// Blocks created by GenerateChain do not contain valid proof of work
|
|
||||||
// values. Inserting them into BlockChain requires use of FakePow or
|
|
||||||
// a similar non-validating proof of work implementation.
|
|
||||||
func GenerateChain(config *params.ChainConfig, parent *types.Block, engine consensus.Engine, db ethdb.Database, n int, gen func(int, *BlockGen)) ([]*types.Block, []types.Receipts) {
|
|
||||||
if config == nil {
|
|
||||||
config = params.TestChainConfig
|
|
||||||
}
|
|
||||||
if engine == nil {
|
|
||||||
panic("nil consensus engine")
|
|
||||||
}
|
|
||||||
cm := newChainMaker(parent, config, engine)
|
|
||||||
|
|
||||||
genblock := func(i int, parent *types.Block, triedb *trie.Database, statedb *state.StateDB) (*types.Block, types.Receipts) {
|
|
||||||
b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine}
|
|
||||||
b.header = cm.makeHeader(parent, statedb, b.engine)
|
|
||||||
|
|
||||||
// Set the difficulty for clique block. The chain maker doesn't have access
|
|
||||||
// to a chain, so the difficulty will be left unset (nil). Set it here to the
|
|
||||||
// correct value.
|
|
||||||
if b.header.Difficulty == nil {
|
|
||||||
if config.TerminalTotalDifficulty == nil {
|
|
||||||
// Clique chain
|
|
||||||
b.header.Difficulty = big.NewInt(2)
|
|
||||||
} else {
|
|
||||||
// Post-merge chain
|
|
||||||
b.header.Difficulty = big.NewInt(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Mutate the state and block according to any hard-fork specs
|
|
||||||
if daoBlock := config.DAOForkBlock; daoBlock != nil {
|
|
||||||
limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange)
|
|
||||||
if b.header.Number.Cmp(daoBlock) >= 0 && b.header.Number.Cmp(limit) < 0 {
|
|
||||||
if config.DAOForkSupport {
|
|
||||||
b.header.Extra = common.CopyBytes(params.DAOForkBlockExtra)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(b.header.Number) == 0 {
|
|
||||||
misc.ApplyDAOHardFork(statedb)
|
|
||||||
}
|
|
||||||
// Execute any user modifications to the block
|
|
||||||
if gen != nil {
|
|
||||||
gen(i, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, b.txs, b.uncles, b.receipts, b.withdrawals)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write state changes to db
|
|
||||||
root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number))
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("state write error: %v", err))
|
|
||||||
}
|
|
||||||
if err = triedb.Commit(root, false); err != nil {
|
|
||||||
panic(fmt.Sprintf("trie write error: %v", err))
|
|
||||||
}
|
|
||||||
return block, b.receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forcibly use hash-based state scheme for retaining all nodes in disk.
|
|
||||||
triedb := trie.NewDatabase(db, trie.HashDefaults)
|
|
||||||
defer triedb.Close()
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
statedb, err := state.New(parent.Root(), state.NewDatabaseWithNodeDB(db, triedb), nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
block, receipts := genblock(i, parent, triedb, statedb)
|
|
||||||
|
|
||||||
// Post-process the receipts.
|
|
||||||
// Here we assign the final block hash and other info into the receipt.
|
|
||||||
// In order for DeriveFields to work, the transaction and receipt lists need to be
|
|
||||||
// of equal length. If AddUncheckedTx or AddUncheckedReceipt are used, there will be
|
|
||||||
// extra ones, so we just trim the lists here.
|
|
||||||
receiptsCount := len(receipts)
|
|
||||||
txs := block.Transactions()
|
|
||||||
if len(receipts) > len(txs) {
|
|
||||||
receipts = receipts[:len(txs)]
|
|
||||||
} else if len(receipts) < len(txs) {
|
|
||||||
txs = txs[:len(receipts)]
|
|
||||||
}
|
|
||||||
var blobGasPrice *big.Int
|
|
||||||
if block.ExcessBlobGas() != nil {
|
|
||||||
blobGasPrice = eip4844.CalcBlobFee(*block.ExcessBlobGas())
|
|
||||||
}
|
|
||||||
if err := receipts.DeriveFields(config, block.Hash(), block.NumberU64(), block.Time(), block.BaseFee(), blobGasPrice, txs); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-expand to ensure all receipts are returned.
|
|
||||||
receipts = receipts[:receiptsCount]
|
|
||||||
|
|
||||||
// Advance the chain.
|
|
||||||
cm.add(block, receipts)
|
|
||||||
parent = block
|
|
||||||
}
|
|
||||||
return cm.chain, cm.receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateChainWithGenesis is a wrapper of GenerateChain which will initialize
|
|
||||||
// genesis block to database first according to the provided genesis specification
|
|
||||||
// then generate chain on top.
|
|
||||||
func GenerateChainWithGenesis(genesis *Genesis, engine consensus.Engine, n int, gen func(int, *BlockGen)) (ethdb.Database, []*types.Block, []types.Receipts) {
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
triedb := trie.NewDatabase(db, trie.HashDefaults)
|
|
||||||
defer triedb.Close()
|
|
||||||
_, err := genesis.Commit(db, triedb)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
blocks, receipts := GenerateChain(genesis.Config, genesis.ToBlock(), engine, db, n, gen)
|
|
||||||
return db, blocks, receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) makeHeader(parent *types.Block, state *state.StateDB, engine consensus.Engine) *types.Header {
|
|
||||||
time := parent.Time() + 10 // block time is fixed at 10 seconds
|
|
||||||
header := &types.Header{
|
|
||||||
Root: state.IntermediateRoot(cm.config.IsEIP158(parent.Number())),
|
|
||||||
ParentHash: parent.Hash(),
|
|
||||||
Coinbase: parent.Coinbase(),
|
|
||||||
Difficulty: engine.CalcDifficulty(cm, time, parent.Header()),
|
|
||||||
GasLimit: parent.GasLimit(),
|
|
||||||
Number: new(big.Int).Add(parent.Number(), common.Big1),
|
|
||||||
Time: time,
|
|
||||||
}
|
|
||||||
|
|
||||||
if cm.config.IsLondon(header.Number) {
|
|
||||||
header.BaseFee = eip1559.CalcBaseFee(cm.config, parent.Header())
|
|
||||||
if !cm.config.IsLondon(parent.Number()) {
|
|
||||||
parentGasLimit := parent.GasLimit() * cm.config.ElasticityMultiplier()
|
|
||||||
header.GasLimit = CalcGasLimit(parentGasLimit, parentGasLimit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cm.config.IsCancun(header.Number, header.Time) {
|
|
||||||
var (
|
|
||||||
parentExcessBlobGas uint64
|
|
||||||
parentBlobGasUsed uint64
|
|
||||||
)
|
|
||||||
if parent.ExcessBlobGas() != nil {
|
|
||||||
parentExcessBlobGas = *parent.ExcessBlobGas()
|
|
||||||
parentBlobGasUsed = *parent.BlobGasUsed()
|
|
||||||
}
|
|
||||||
excessBlobGas := eip4844.CalcExcessBlobGas(parentExcessBlobGas, parentBlobGasUsed)
|
|
||||||
header.ExcessBlobGas = &excessBlobGas
|
|
||||||
header.BlobGasUsed = new(uint64)
|
|
||||||
header.ParentBeaconRoot = new(common.Hash)
|
|
||||||
}
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeHeaderChain creates a deterministic chain of headers rooted at parent.
|
|
||||||
func makeHeaderChain(chainConfig *params.ChainConfig, parent *types.Header, n int, engine consensus.Engine, db ethdb.Database, seed int) []*types.Header {
|
|
||||||
blocks := makeBlockChain(chainConfig, types.NewBlockWithHeader(parent), n, engine, db, seed)
|
|
||||||
headers := make([]*types.Header, len(blocks))
|
|
||||||
for i, block := range blocks {
|
|
||||||
headers[i] = block.Header()
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeHeaderChainWithGenesis creates a deterministic chain of headers from genesis.
|
|
||||||
func makeHeaderChainWithGenesis(genesis *Genesis, n int, engine consensus.Engine, seed int) (ethdb.Database, []*types.Header) {
|
|
||||||
db, blocks := makeBlockChainWithGenesis(genesis, n, engine, seed)
|
|
||||||
headers := make([]*types.Header, len(blocks))
|
|
||||||
for i, block := range blocks {
|
|
||||||
headers[i] = block.Header()
|
|
||||||
}
|
|
||||||
return db, headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeBlockChain creates a deterministic chain of blocks rooted at parent.
|
|
||||||
func makeBlockChain(chainConfig *params.ChainConfig, parent *types.Block, n int, engine consensus.Engine, db ethdb.Database, seed int) []*types.Block {
|
|
||||||
blocks, _ := GenerateChain(chainConfig, parent, engine, db, n, func(i int, b *BlockGen) {
|
|
||||||
b.SetCoinbase(common.Address{0: byte(seed), 19: byte(i)})
|
|
||||||
})
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeBlockChain creates a deterministic chain of blocks from genesis
|
|
||||||
func makeBlockChainWithGenesis(genesis *Genesis, n int, engine consensus.Engine, seed int) (ethdb.Database, []*types.Block) {
|
|
||||||
db, blocks, _ := GenerateChainWithGenesis(genesis, engine, n, func(i int, b *BlockGen) {
|
|
||||||
b.SetCoinbase(common.Address{0: byte(seed), 19: byte(i)})
|
|
||||||
})
|
|
||||||
return db, blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
// chainMaker contains the state of chain generation.
|
|
||||||
type chainMaker struct {
|
|
||||||
bottom *types.Block
|
|
||||||
engine consensus.Engine
|
|
||||||
config *params.ChainConfig
|
|
||||||
chain []*types.Block
|
|
||||||
chainByHash map[common.Hash]*types.Block
|
|
||||||
receipts []types.Receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChainMaker(bottom *types.Block, config *params.ChainConfig, engine consensus.Engine) *chainMaker {
|
|
||||||
return &chainMaker{
|
|
||||||
bottom: bottom,
|
|
||||||
config: config,
|
|
||||||
engine: engine,
|
|
||||||
chainByHash: make(map[common.Hash]*types.Block),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) add(b *types.Block, r []*types.Receipt) {
|
|
||||||
cm.chain = append(cm.chain, b)
|
|
||||||
cm.chainByHash[b.Hash()] = b
|
|
||||||
cm.receipts = append(cm.receipts, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) blockByNumber(number uint64) *types.Block {
|
|
||||||
if number == cm.bottom.NumberU64() {
|
|
||||||
return cm.bottom
|
|
||||||
}
|
|
||||||
cur := cm.CurrentHeader().Number.Uint64()
|
|
||||||
lowest := cm.bottom.NumberU64() + 1
|
|
||||||
if number < lowest || number > cur {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cm.chain[number-lowest]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainReader/ChainContext implementation
|
|
||||||
|
|
||||||
// Config returns the chain configuration (for consensus.ChainReader).
|
|
||||||
func (cm *chainMaker) Config() *params.ChainConfig {
|
|
||||||
return cm.config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Engine returns the consensus engine (for ChainContext).
|
|
||||||
func (cm *chainMaker) Engine() consensus.Engine {
|
|
||||||
return cm.engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) CurrentHeader() *types.Header {
|
|
||||||
if len(cm.chain) == 0 {
|
|
||||||
return cm.bottom.Header()
|
|
||||||
}
|
|
||||||
return cm.chain[len(cm.chain)-1].Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) GetHeaderByNumber(number uint64) *types.Header {
|
|
||||||
b := cm.blockByNumber(number)
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return b.Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) GetHeaderByHash(hash common.Hash) *types.Header {
|
|
||||||
b := cm.chainByHash[hash]
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return b.Header()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) GetHeader(hash common.Hash, number uint64) *types.Header {
|
|
||||||
return cm.GetHeaderByNumber(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) GetBlock(hash common.Hash, number uint64) *types.Block {
|
|
||||||
return cm.blockByNumber(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *chainMaker) GetTd(hash common.Hash, number uint64) *big.Int {
|
|
||||||
return nil // not supported
|
|
||||||
}
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/beacon"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeneratePOSChain(t *testing.T) {
|
|
||||||
var (
|
|
||||||
keyHex = "9c647b8b7c4e7c3490668fb6c11473619db80c93704c70893d3813af4090c39c"
|
|
||||||
key, _ = crypto.HexToECDSA(keyHex)
|
|
||||||
address = crypto.PubkeyToAddress(key.PublicKey) // 658bdf435d810c91414ec09147daa6db62406379
|
|
||||||
aa = common.Address{0xaa}
|
|
||||||
bb = common.Address{0xbb}
|
|
||||||
funds = big.NewInt(0).Mul(big.NewInt(1337), big.NewInt(params.Ether))
|
|
||||||
config = *params.AllEthashProtocolChanges
|
|
||||||
asm4788 = common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500")
|
|
||||||
gspec = &Genesis{
|
|
||||||
Config: &config,
|
|
||||||
Alloc: GenesisAlloc{
|
|
||||||
address: {Balance: funds},
|
|
||||||
params.BeaconRootsStorageAddress: {Balance: common.Big0, Code: asm4788},
|
|
||||||
},
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Difficulty: common.Big1,
|
|
||||||
GasLimit: 5_000_000,
|
|
||||||
}
|
|
||||||
gendb = rawdb.NewMemoryDatabase()
|
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
)
|
|
||||||
|
|
||||||
config.TerminalTotalDifficultyPassed = true
|
|
||||||
config.TerminalTotalDifficulty = common.Big0
|
|
||||||
config.ShanghaiTime = u64(0)
|
|
||||||
config.CancunTime = u64(0)
|
|
||||||
|
|
||||||
// init 0xaa with some storage elements
|
|
||||||
storage := make(map[common.Hash]common.Hash)
|
|
||||||
storage[common.Hash{0x00}] = common.Hash{0x00}
|
|
||||||
storage[common.Hash{0x01}] = common.Hash{0x01}
|
|
||||||
storage[common.Hash{0x02}] = common.Hash{0x02}
|
|
||||||
storage[common.Hash{0x03}] = common.HexToHash("0303")
|
|
||||||
gspec.Alloc[aa] = GenesisAccount{
|
|
||||||
Balance: common.Big1,
|
|
||||||
Nonce: 1,
|
|
||||||
Storage: storage,
|
|
||||||
Code: common.Hex2Bytes("6042"),
|
|
||||||
}
|
|
||||||
gspec.Alloc[bb] = GenesisAccount{
|
|
||||||
Balance: common.Big2,
|
|
||||||
Nonce: 1,
|
|
||||||
Storage: storage,
|
|
||||||
Code: common.Hex2Bytes("600154600354"),
|
|
||||||
}
|
|
||||||
genesis := gspec.MustCommit(gendb, trie.NewDatabase(gendb, trie.HashDefaults))
|
|
||||||
|
|
||||||
genchain, genreceipts := GenerateChain(gspec.Config, genesis, beacon.NewFaker(), gendb, 4, func(i int, gen *BlockGen) {
|
|
||||||
gen.SetParentBeaconRoot(common.Hash{byte(i + 1)})
|
|
||||||
|
|
||||||
// Add value transfer tx.
|
|
||||||
tx := types.MustSignNewTx(key, gen.Signer(), &types.LegacyTx{
|
|
||||||
Nonce: gen.TxNonce(address),
|
|
||||||
To: &address,
|
|
||||||
Value: big.NewInt(1000),
|
|
||||||
Gas: params.TxGas,
|
|
||||||
GasPrice: new(big.Int).Add(gen.BaseFee(), common.Big1),
|
|
||||||
})
|
|
||||||
gen.AddTx(tx)
|
|
||||||
|
|
||||||
// Add withdrawals.
|
|
||||||
if i == 1 {
|
|
||||||
gen.AddWithdrawal(&types.Withdrawal{
|
|
||||||
Validator: 42,
|
|
||||||
Address: common.Address{0xee},
|
|
||||||
Amount: 1337,
|
|
||||||
})
|
|
||||||
gen.AddWithdrawal(&types.Withdrawal{
|
|
||||||
Validator: 13,
|
|
||||||
Address: common.Address{0xee},
|
|
||||||
Amount: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if i == 3 {
|
|
||||||
gen.AddWithdrawal(&types.Withdrawal{
|
|
||||||
Validator: 42,
|
|
||||||
Address: common.Address{0xee},
|
|
||||||
Amount: 1337,
|
|
||||||
})
|
|
||||||
gen.AddWithdrawal(&types.Withdrawal{
|
|
||||||
Validator: 13,
|
|
||||||
Address: common.Address{0xee},
|
|
||||||
Amount: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Import the chain. This runs all block validation rules.
|
|
||||||
blockchain, _ := NewBlockChain(db, nil, gspec, nil, beacon.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer blockchain.Stop()
|
|
||||||
|
|
||||||
if i, err := blockchain.InsertChain(genchain); err != nil {
|
|
||||||
t.Fatalf("insert error (block %d): %v\n", genchain[i].NumberU64(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforce that withdrawal indexes are monotonically increasing from 0
|
|
||||||
var (
|
|
||||||
withdrawalIndex uint64
|
|
||||||
)
|
|
||||||
for i := range genchain {
|
|
||||||
blocknum := genchain[i].NumberU64()
|
|
||||||
block := blockchain.GetBlockByNumber(blocknum)
|
|
||||||
if block == nil {
|
|
||||||
t.Fatalf("block %d not found", blocknum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify receipts.
|
|
||||||
genBlockReceipts := genreceipts[i]
|
|
||||||
for _, r := range genBlockReceipts {
|
|
||||||
if r.BlockNumber.Cmp(block.Number()) != 0 {
|
|
||||||
t.Errorf("receipt has wrong block number %d, want %d", r.BlockNumber, block.Number())
|
|
||||||
}
|
|
||||||
if r.BlockHash != block.Hash() {
|
|
||||||
t.Errorf("receipt has wrong block hash %v, want %v", r.BlockHash, block.Hash())
|
|
||||||
}
|
|
||||||
|
|
||||||
// patch up empty logs list to make DeepEqual below work
|
|
||||||
if r.Logs == nil {
|
|
||||||
r.Logs = []*types.Log{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blockchainReceipts := blockchain.GetReceiptsByHash(block.Hash())
|
|
||||||
if !reflect.DeepEqual(genBlockReceipts, blockchainReceipts) {
|
|
||||||
t.Fatalf("receipts mismatch\ngenerated: %s\nblockchain: %s", spew.Sdump(genBlockReceipts), spew.Sdump(blockchainReceipts))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify withdrawals.
|
|
||||||
if len(block.Withdrawals()) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for j := 0; j < len(block.Withdrawals()); j++ {
|
|
||||||
if block.Withdrawals()[j].Index != withdrawalIndex {
|
|
||||||
t.Fatalf("withdrawal index %d does not equal expected index %d", block.Withdrawals()[j].Index, withdrawalIndex)
|
|
||||||
}
|
|
||||||
withdrawalIndex += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify parent beacon root.
|
|
||||||
want := common.Hash{byte(blocknum)}
|
|
||||||
if got := block.BeaconRoot(); *got != want {
|
|
||||||
t.Fatalf("block %d, wrong parent beacon root: got %s, want %s", i, got, want)
|
|
||||||
}
|
|
||||||
state, _ := blockchain.State()
|
|
||||||
idx := block.Time()%8191 + 8191
|
|
||||||
got := state.GetState(params.BeaconRootsStorageAddress, common.BigToHash(new(big.Int).SetUint64(idx)))
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("block %d, wrong parent beacon root in state: got %s, want %s", i, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleGenerateChain() {
|
|
||||||
var (
|
|
||||||
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
|
|
||||||
key3, _ = crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee")
|
|
||||||
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
|
|
||||||
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
|
|
||||||
addr3 = crypto.PubkeyToAddress(key3.PublicKey)
|
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
genDb = rawdb.NewMemoryDatabase()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure that key1 has some funds in the genesis block.
|
|
||||||
gspec := &Genesis{
|
|
||||||
Config: ¶ms.ChainConfig{HomesteadBlock: new(big.Int)},
|
|
||||||
Alloc: GenesisAlloc{addr1: {Balance: big.NewInt(1000000)}},
|
|
||||||
}
|
|
||||||
genesis := gspec.MustCommit(genDb, trie.NewDatabase(genDb, trie.HashDefaults))
|
|
||||||
|
|
||||||
// This call generates a chain of 5 blocks. The function runs for
|
|
||||||
// each block and adds different features to gen based on the
|
|
||||||
// block index.
|
|
||||||
signer := types.HomesteadSigner{}
|
|
||||||
chain, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), genDb, 5, func(i int, gen *BlockGen) {
|
|
||||||
switch i {
|
|
||||||
case 0:
|
|
||||||
// In block 1, addr1 sends addr2 some ether.
|
|
||||||
tx, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr1), addr2, big.NewInt(10000), params.TxGas, nil, nil), signer, key1)
|
|
||||||
gen.AddTx(tx)
|
|
||||||
case 1:
|
|
||||||
// In block 2, addr1 sends some more ether to addr2.
|
|
||||||
// addr2 passes it on to addr3.
|
|
||||||
tx1, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr1), addr2, big.NewInt(1000), params.TxGas, nil, nil), signer, key1)
|
|
||||||
tx2, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr2), addr3, big.NewInt(1000), params.TxGas, nil, nil), signer, key2)
|
|
||||||
gen.AddTx(tx1)
|
|
||||||
gen.AddTx(tx2)
|
|
||||||
case 2:
|
|
||||||
// Block 3 is empty but was mined by addr3.
|
|
||||||
gen.SetCoinbase(addr3)
|
|
||||||
gen.SetExtra([]byte("yeehaw"))
|
|
||||||
case 3:
|
|
||||||
// Block 4 includes blocks 2 and 3 as uncle headers (with modified extra data).
|
|
||||||
b2 := gen.PrevBlock(1).Header()
|
|
||||||
b2.Extra = []byte("foo")
|
|
||||||
gen.AddUncle(b2)
|
|
||||||
b3 := gen.PrevBlock(2).Header()
|
|
||||||
b3.Extra = []byte("foo")
|
|
||||||
gen.AddUncle(b3)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Import the chain. This runs all block validation rules.
|
|
||||||
blockchain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.HashScheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer blockchain.Stop()
|
|
||||||
|
|
||||||
if i, err := blockchain.InsertChain(chain); err != nil {
|
|
||||||
fmt.Printf("insert error (block %d): %v\n", chain[i].NumberU64(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state, _ := blockchain.State()
|
|
||||||
fmt.Printf("last block: #%d\n", blockchain.CurrentBlock().Number)
|
|
||||||
fmt.Println("balance of addr1:", state.GetBalance(addr1))
|
|
||||||
fmt.Println("balance of addr2:", state.GetBalance(addr2))
|
|
||||||
fmt.Println("balance of addr3:", state.GetBalance(addr3))
|
|
||||||
// Output:
|
|
||||||
// last block: #5
|
|
||||||
// balance of addr1: 989000
|
|
||||||
// balance of addr2: 10000
|
|
||||||
// balance of addr3: 19687500000000001000
|
|
||||||
}
|
|
||||||
159
core/dao_test.go
159
core/dao_test.go
|
|
@ -1,159 +0,0 @@
|
||||||
// Copyright 2016 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that DAO-fork enabled clients can properly filter out fork-commencing
|
|
||||||
// blocks based on their extradata fields.
|
|
||||||
func TestDAOForkRangeExtradata(t *testing.T) {
|
|
||||||
forkBlock := big.NewInt(32)
|
|
||||||
chainConfig := *params.NonActivatedConfig
|
|
||||||
chainConfig.HomesteadBlock = big.NewInt(0)
|
|
||||||
|
|
||||||
// Generate a common prefix for both pro-forkers and non-forkers
|
|
||||||
gspec := &Genesis{
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Config: &chainConfig,
|
|
||||||
}
|
|
||||||
genDb, prefix, _ := GenerateChainWithGenesis(gspec, ethash.NewFaker(), int(forkBlock.Int64()-1), func(i int, gen *BlockGen) {})
|
|
||||||
|
|
||||||
// Create the concurrent, conflicting two nodes
|
|
||||||
proDb := rawdb.NewMemoryDatabase()
|
|
||||||
proConf := *params.NonActivatedConfig
|
|
||||||
proConf.HomesteadBlock = big.NewInt(0)
|
|
||||||
proConf.DAOForkBlock = forkBlock
|
|
||||||
proConf.DAOForkSupport = true
|
|
||||||
progspec := &Genesis{
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Config: &proConf,
|
|
||||||
}
|
|
||||||
proBc, _ := NewBlockChain(proDb, nil, progspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer proBc.Stop()
|
|
||||||
|
|
||||||
conDb := rawdb.NewMemoryDatabase()
|
|
||||||
conConf := *params.NonActivatedConfig
|
|
||||||
conConf.HomesteadBlock = big.NewInt(0)
|
|
||||||
conConf.DAOForkBlock = forkBlock
|
|
||||||
conConf.DAOForkSupport = false
|
|
||||||
congspec := &Genesis{
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Config: &conConf,
|
|
||||||
}
|
|
||||||
conBc, _ := NewBlockChain(conDb, nil, congspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer conBc.Stop()
|
|
||||||
|
|
||||||
if _, err := proBc.InsertChain(prefix); err != nil {
|
|
||||||
t.Fatalf("pro-fork: failed to import chain prefix: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := conBc.InsertChain(prefix); err != nil {
|
|
||||||
t.Fatalf("con-fork: failed to import chain prefix: %v", err)
|
|
||||||
}
|
|
||||||
// Try to expand both pro-fork and non-fork chains iteratively with other camp's blocks
|
|
||||||
for i := int64(0); i < params.DAOForkExtraRange.Int64(); i++ {
|
|
||||||
// Create a pro-fork block, and try to feed into the no-fork chain
|
|
||||||
bc, _ := NewBlockChain(rawdb.NewMemoryDatabase(), nil, congspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
|
|
||||||
blocks := conBc.GetBlocksFromHash(conBc.CurrentBlock().Hash(), int(conBc.CurrentBlock().Number.Uint64()))
|
|
||||||
for j := 0; j < len(blocks)/2; j++ {
|
|
||||||
blocks[j], blocks[len(blocks)-1-j] = blocks[len(blocks)-1-j], blocks[j]
|
|
||||||
}
|
|
||||||
if _, err := bc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("failed to import contra-fork chain for expansion: %v", err)
|
|
||||||
}
|
|
||||||
if err := bc.triedb.Commit(bc.CurrentHeader().Root, false); err != nil {
|
|
||||||
t.Fatalf("failed to commit contra-fork head for expansion: %v", err)
|
|
||||||
}
|
|
||||||
bc.Stop()
|
|
||||||
blocks, _ = GenerateChain(&proConf, conBc.GetBlockByHash(conBc.CurrentBlock().Hash()), ethash.NewFaker(), genDb, 1, func(i int, gen *BlockGen) {})
|
|
||||||
if _, err := conBc.InsertChain(blocks); err == nil {
|
|
||||||
t.Fatalf("contra-fork chain accepted pro-fork block: %v", blocks[0])
|
|
||||||
}
|
|
||||||
// Create a proper no-fork block for the contra-forker
|
|
||||||
blocks, _ = GenerateChain(&conConf, conBc.GetBlockByHash(conBc.CurrentBlock().Hash()), ethash.NewFaker(), genDb, 1, func(i int, gen *BlockGen) {})
|
|
||||||
if _, err := conBc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("contra-fork chain didn't accepted no-fork block: %v", err)
|
|
||||||
}
|
|
||||||
// Create a no-fork block, and try to feed into the pro-fork chain
|
|
||||||
bc, _ = NewBlockChain(rawdb.NewMemoryDatabase(), nil, progspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
|
|
||||||
blocks = proBc.GetBlocksFromHash(proBc.CurrentBlock().Hash(), int(proBc.CurrentBlock().Number.Uint64()))
|
|
||||||
for j := 0; j < len(blocks)/2; j++ {
|
|
||||||
blocks[j], blocks[len(blocks)-1-j] = blocks[len(blocks)-1-j], blocks[j]
|
|
||||||
}
|
|
||||||
if _, err := bc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("failed to import pro-fork chain for expansion: %v", err)
|
|
||||||
}
|
|
||||||
if err := bc.triedb.Commit(bc.CurrentHeader().Root, false); err != nil {
|
|
||||||
t.Fatalf("failed to commit pro-fork head for expansion: %v", err)
|
|
||||||
}
|
|
||||||
bc.Stop()
|
|
||||||
blocks, _ = GenerateChain(&conConf, proBc.GetBlockByHash(proBc.CurrentBlock().Hash()), ethash.NewFaker(), genDb, 1, func(i int, gen *BlockGen) {})
|
|
||||||
if _, err := proBc.InsertChain(blocks); err == nil {
|
|
||||||
t.Fatalf("pro-fork chain accepted contra-fork block: %v", blocks[0])
|
|
||||||
}
|
|
||||||
// Create a proper pro-fork block for the pro-forker
|
|
||||||
blocks, _ = GenerateChain(&proConf, proBc.GetBlockByHash(proBc.CurrentBlock().Hash()), ethash.NewFaker(), genDb, 1, func(i int, gen *BlockGen) {})
|
|
||||||
if _, err := proBc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("pro-fork chain didn't accepted pro-fork block: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Verify that contra-forkers accept pro-fork extra-datas after forking finishes
|
|
||||||
bc, _ := NewBlockChain(rawdb.NewMemoryDatabase(), nil, congspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer bc.Stop()
|
|
||||||
|
|
||||||
blocks := conBc.GetBlocksFromHash(conBc.CurrentBlock().Hash(), int(conBc.CurrentBlock().Number.Uint64()))
|
|
||||||
for j := 0; j < len(blocks)/2; j++ {
|
|
||||||
blocks[j], blocks[len(blocks)-1-j] = blocks[len(blocks)-1-j], blocks[j]
|
|
||||||
}
|
|
||||||
if _, err := bc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("failed to import contra-fork chain for expansion: %v", err)
|
|
||||||
}
|
|
||||||
if err := bc.triedb.Commit(bc.CurrentHeader().Root, false); err != nil {
|
|
||||||
t.Fatalf("failed to commit contra-fork head for expansion: %v", err)
|
|
||||||
}
|
|
||||||
blocks, _ = GenerateChain(&proConf, conBc.GetBlockByHash(conBc.CurrentBlock().Hash()), ethash.NewFaker(), genDb, 1, func(i int, gen *BlockGen) {})
|
|
||||||
if _, err := conBc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("contra-fork chain didn't accept pro-fork block post-fork: %v", err)
|
|
||||||
}
|
|
||||||
// Verify that pro-forkers accept contra-fork extra-datas after forking finishes
|
|
||||||
bc, _ = NewBlockChain(rawdb.NewMemoryDatabase(), nil, progspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer bc.Stop()
|
|
||||||
|
|
||||||
blocks = proBc.GetBlocksFromHash(proBc.CurrentBlock().Hash(), int(proBc.CurrentBlock().Number.Uint64()))
|
|
||||||
for j := 0; j < len(blocks)/2; j++ {
|
|
||||||
blocks[j], blocks[len(blocks)-1-j] = blocks[len(blocks)-1-j], blocks[j]
|
|
||||||
}
|
|
||||||
if _, err := bc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("failed to import pro-fork chain for expansion: %v", err)
|
|
||||||
}
|
|
||||||
if err := bc.triedb.Commit(bc.CurrentHeader().Root, false); err != nil {
|
|
||||||
t.Fatalf("failed to commit pro-fork head for expansion: %v", err)
|
|
||||||
}
|
|
||||||
blocks, _ = GenerateChain(&conConf, proBc.GetBlockByHash(proBc.CurrentBlock().Hash()), ethash.NewFaker(), genDb, 1, func(i int, gen *BlockGen) {})
|
|
||||||
if _, err := proBc.InsertChain(blocks); err != nil {
|
|
||||||
t.Fatalf("pro-fork chain didn't accept contra-fork block post-fork: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
107
core/error.go
107
core/error.go
|
|
@ -1,107 +0,0 @@
|
||||||
// Copyright 2014 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrKnownBlock is returned when a block to import is already known locally.
|
|
||||||
ErrKnownBlock = errors.New("block already known")
|
|
||||||
|
|
||||||
// ErrBannedHash is returned if a block to import is on the banned list.
|
|
||||||
ErrBannedHash = errors.New("banned hash")
|
|
||||||
|
|
||||||
// ErrNoGenesis is returned when there is no Genesis Block.
|
|
||||||
ErrNoGenesis = errors.New("genesis not found in chain")
|
|
||||||
|
|
||||||
errSideChainReceipts = errors.New("side blocks can't be accepted as ancient chain data")
|
|
||||||
)
|
|
||||||
|
|
||||||
// List of evm-call-message pre-checking errors. All state transition messages will
|
|
||||||
// be pre-checked before execution. If any invalidation detected, the corresponding
|
|
||||||
// error should be returned which is defined here.
|
|
||||||
//
|
|
||||||
// - If the pre-checking happens in the miner, then the transaction won't be packed.
|
|
||||||
// - If the pre-checking happens in the block processing procedure, then a "BAD BLOCk"
|
|
||||||
// error should be emitted.
|
|
||||||
var (
|
|
||||||
// ErrNonceTooLow is returned if the nonce of a transaction is lower than the
|
|
||||||
// one present in the local chain.
|
|
||||||
ErrNonceTooLow = errors.New("nonce too low")
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
|
|
||||||
// ErrNonceMax is returned if the nonce of a transaction sender account has
|
|
||||||
// maximum allowed value and would become invalid if incremented.
|
|
||||||
ErrNonceMax = errors.New("nonce has max value")
|
|
||||||
|
|
||||||
// ErrGasLimitReached is returned by the gas pool if the amount of gas required
|
|
||||||
// by a transaction is higher than what's left in the block.
|
|
||||||
ErrGasLimitReached = errors.New("gas limit reached")
|
|
||||||
|
|
||||||
// ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't
|
|
||||||
// have enough funds for transfer(topmost call only).
|
|
||||||
ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer")
|
|
||||||
|
|
||||||
// ErrMaxInitCodeSizeExceeded is returned if creation transaction provides the init code bigger
|
|
||||||
// than init code size limit.
|
|
||||||
ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded")
|
|
||||||
|
|
||||||
// ErrInsufficientFunds is returned if the total cost of executing a transaction
|
|
||||||
// is higher than the balance of the user's account.
|
|
||||||
ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value")
|
|
||||||
|
|
||||||
// ErrGasUintOverflow is returned when calculating gas usage.
|
|
||||||
ErrGasUintOverflow = errors.New("gas uint64 overflow")
|
|
||||||
|
|
||||||
// ErrIntrinsicGas is returned if the transaction is specified to use less gas
|
|
||||||
// than required to start the invocation.
|
|
||||||
ErrIntrinsicGas = errors.New("intrinsic gas too low")
|
|
||||||
|
|
||||||
// ErrTxTypeNotSupported is returned if a transaction is not supported in the
|
|
||||||
// current network configuration.
|
|
||||||
ErrTxTypeNotSupported = types.ErrTxTypeNotSupported
|
|
||||||
|
|
||||||
// ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a
|
|
||||||
// transaction with a tip higher than the total fee cap.
|
|
||||||
ErrTipAboveFeeCap = errors.New("max priority fee per gas higher than max fee per gas")
|
|
||||||
|
|
||||||
// ErrTipVeryHigh is a sanity error to avoid extremely big numbers specified
|
|
||||||
// in the tip field.
|
|
||||||
ErrTipVeryHigh = errors.New("max priority fee per gas higher than 2^256-1")
|
|
||||||
|
|
||||||
// ErrFeeCapVeryHigh is a sanity error to avoid extremely big numbers specified
|
|
||||||
// in the fee cap field.
|
|
||||||
ErrFeeCapVeryHigh = errors.New("max fee per gas higher than 2^256-1")
|
|
||||||
|
|
||||||
// ErrFeeCapTooLow is returned if the transaction fee cap is less than the
|
|
||||||
// base fee of the block.
|
|
||||||
ErrFeeCapTooLow = errors.New("max fee per gas less than block base fee")
|
|
||||||
|
|
||||||
// ErrSenderNoEOA is returned if the sender of a transaction is a contract.
|
|
||||||
ErrSenderNoEOA = errors.New("sender not an eoa")
|
|
||||||
|
|
||||||
// ErrBlobFeeCapTooLow is returned if the transaction fee cap is less than the
|
|
||||||
// blob gas fee of the block.
|
|
||||||
ErrBlobFeeCapTooLow = errors.New("max fee per blob gas less than block blob gas fee")
|
|
||||||
)
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// Copyright 2014 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewTxsEvent is posted when a batch of transactions enter the transaction pool.
|
|
||||||
type NewTxsEvent struct{ Txs []*types.Transaction }
|
|
||||||
|
|
||||||
// NewMinedBlockEvent is posted when a block has been imported.
|
|
||||||
type NewMinedBlockEvent struct{ Block *types.Block }
|
|
||||||
|
|
||||||
// RemovedLogsEvent is posted when a reorg happens
|
|
||||||
type RemovedLogsEvent struct{ Logs []*types.Log }
|
|
||||||
|
|
||||||
type ChainEvent struct {
|
|
||||||
Block *types.Block
|
|
||||||
Hash common.Hash
|
|
||||||
Logs []*types.Log
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChainSideEvent struct {
|
|
||||||
Block *types.Block
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChainHeadEvent struct{ Block *types.Block }
|
|
||||||
140
core/evm.go
140
core/evm.go
|
|
@ -1,140 +0,0 @@
|
||||||
// Copyright 2016 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChainContext supports retrieving headers and consensus parameters from the
|
|
||||||
// current blockchain to be used during transaction processing.
|
|
||||||
type ChainContext interface {
|
|
||||||
// Engine retrieves the chain's consensus engine.
|
|
||||||
Engine() consensus.Engine
|
|
||||||
|
|
||||||
// GetHeader returns the header corresponding to the hash/number argument pair.
|
|
||||||
GetHeader(common.Hash, uint64) *types.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEVMBlockContext creates a new context for use in the EVM.
|
|
||||||
func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext {
|
|
||||||
var (
|
|
||||||
beneficiary common.Address
|
|
||||||
baseFee *big.Int
|
|
||||||
blobBaseFee *big.Int
|
|
||||||
random *common.Hash
|
|
||||||
)
|
|
||||||
|
|
||||||
// If we don't have an explicit author (i.e. not mining), extract from the header
|
|
||||||
if author == nil {
|
|
||||||
beneficiary, _ = chain.Engine().Author(header) // Ignore error, we're past header validation
|
|
||||||
} else {
|
|
||||||
beneficiary = *author
|
|
||||||
}
|
|
||||||
if header.BaseFee != nil {
|
|
||||||
baseFee = new(big.Int).Set(header.BaseFee)
|
|
||||||
}
|
|
||||||
if header.ExcessBlobGas != nil {
|
|
||||||
blobBaseFee = eip4844.CalcBlobFee(*header.ExcessBlobGas)
|
|
||||||
}
|
|
||||||
if header.Difficulty.Cmp(common.Big0) == 0 {
|
|
||||||
random = &header.MixDigest
|
|
||||||
}
|
|
||||||
return vm.BlockContext{
|
|
||||||
CanTransfer: CanTransfer,
|
|
||||||
Transfer: Transfer,
|
|
||||||
GetHash: GetHashFn(header, chain),
|
|
||||||
Coinbase: beneficiary,
|
|
||||||
BlockNumber: new(big.Int).Set(header.Number),
|
|
||||||
Time: header.Time,
|
|
||||||
Difficulty: new(big.Int).Set(header.Difficulty),
|
|
||||||
BaseFee: baseFee,
|
|
||||||
BlobBaseFee: blobBaseFee,
|
|
||||||
GasLimit: header.GasLimit,
|
|
||||||
Random: random,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEVMTxContext creates a new transaction context for a single transaction.
|
|
||||||
func NewEVMTxContext(msg *Message) vm.TxContext {
|
|
||||||
ctx := vm.TxContext{
|
|
||||||
Origin: msg.From,
|
|
||||||
GasPrice: new(big.Int).Set(msg.GasPrice),
|
|
||||||
BlobHashes: msg.BlobHashes,
|
|
||||||
}
|
|
||||||
if msg.BlobGasFeeCap != nil {
|
|
||||||
ctx.BlobFeeCap = new(big.Int).Set(msg.BlobGasFeeCap)
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHashFn returns a GetHashFunc which retrieves header hashes by number
|
|
||||||
func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash {
|
|
||||||
// Cache will initially contain [refHash.parent],
|
|
||||||
// Then fill up with [refHash.p, refHash.pp, refHash.ppp, ...]
|
|
||||||
var cache []common.Hash
|
|
||||||
|
|
||||||
return func(n uint64) common.Hash {
|
|
||||||
if ref.Number.Uint64() <= n {
|
|
||||||
// This situation can happen if we're doing tracing and using
|
|
||||||
// block overrides.
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
// If there's no hash cache yet, make one
|
|
||||||
if len(cache) == 0 {
|
|
||||||
cache = append(cache, ref.ParentHash)
|
|
||||||
}
|
|
||||||
if idx := ref.Number.Uint64() - n - 1; idx < uint64(len(cache)) {
|
|
||||||
return cache[idx]
|
|
||||||
}
|
|
||||||
// No luck in the cache, but we can start iterating from the last element we already know
|
|
||||||
lastKnownHash := cache[len(cache)-1]
|
|
||||||
lastKnownNumber := ref.Number.Uint64() - uint64(len(cache))
|
|
||||||
|
|
||||||
for {
|
|
||||||
header := chain.GetHeader(lastKnownHash, lastKnownNumber)
|
|
||||||
if header == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cache = append(cache, header.ParentHash)
|
|
||||||
lastKnownHash = header.ParentHash
|
|
||||||
lastKnownNumber = header.Number.Uint64() - 1
|
|
||||||
if n == lastKnownNumber {
|
|
||||||
return lastKnownHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanTransfer checks whether there are enough funds in the address' account to make a transfer.
|
|
||||||
// This does not take the necessary gas in to account to make the transfer valid.
|
|
||||||
func CanTransfer(db vm.StateDB, addr common.Address, amount *big.Int) bool {
|
|
||||||
return db.GetBalance(addr).Cmp(amount) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer subtracts amount from sender and adds amount to recipient using the given Db
|
|
||||||
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
|
|
||||||
db.SubBalance(sender, amount)
|
|
||||||
db.AddBalance(recipient, amount)
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
crand "crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"math/big"
|
|
||||||
mrand "math/rand"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChainReader defines a small collection of methods needed to access the local
|
|
||||||
// blockchain during header verification. It's implemented by both blockchain
|
|
||||||
// and lightchain.
|
|
||||||
type ChainReader interface {
|
|
||||||
// Config retrieves the header chain's chain configuration.
|
|
||||||
Config() *params.ChainConfig
|
|
||||||
|
|
||||||
// GetTd returns the total difficulty of a local block.
|
|
||||||
GetTd(common.Hash, uint64) *big.Int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForkChoice is the fork chooser based on the highest total difficulty of the
|
|
||||||
// chain(the fork choice used in the eth1) and the external fork choice (the fork
|
|
||||||
// choice used in the eth2). This main goal of this ForkChoice is not only for
|
|
||||||
// offering fork choice during the eth1/2 merge phase, but also keep the compatibility
|
|
||||||
// for all other proof-of-work networks.
|
|
||||||
type ForkChoice struct {
|
|
||||||
chain ChainReader
|
|
||||||
rand *mrand.Rand
|
|
||||||
|
|
||||||
// preserve is a helper function used in td fork choice.
|
|
||||||
// Miners will prefer to choose the local mined block if the
|
|
||||||
// local td is equal to the extern one. It can be nil for light
|
|
||||||
// client
|
|
||||||
preserve func(header *types.Header) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewForkChoice(chainReader ChainReader, preserve func(header *types.Header) bool) *ForkChoice {
|
|
||||||
// Seed a fast but crypto originating random generator
|
|
||||||
seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to initialize random seed", "err", err)
|
|
||||||
}
|
|
||||||
return &ForkChoice{
|
|
||||||
chain: chainReader,
|
|
||||||
rand: mrand.New(mrand.NewSource(seed.Int64())),
|
|
||||||
preserve: preserve,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorgNeeded returns whether the reorg should be applied
|
|
||||||
// based on the given external header and local canonical chain.
|
|
||||||
// In the td mode, the new head is chosen if the corresponding
|
|
||||||
// total difficulty is higher. In the extern mode, the trusted
|
|
||||||
// header is always selected as the head.
|
|
||||||
func (f *ForkChoice) ReorgNeeded(current *types.Header, extern *types.Header) (bool, error) {
|
|
||||||
var (
|
|
||||||
localTD = f.chain.GetTd(current.Hash(), current.Number.Uint64())
|
|
||||||
externTd = f.chain.GetTd(extern.Hash(), extern.Number.Uint64())
|
|
||||||
)
|
|
||||||
if localTD == nil || externTd == nil {
|
|
||||||
return false, errors.New("missing td")
|
|
||||||
}
|
|
||||||
// Accept the new header as the chain head if the transition
|
|
||||||
// is already triggered. We assume all the headers after the
|
|
||||||
// transition come from the trusted consensus layer.
|
|
||||||
if ttd := f.chain.Config().TerminalTotalDifficulty; ttd != nil && ttd.Cmp(externTd) <= 0 {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the total difficulty is higher than our known, add it to the canonical chain
|
|
||||||
if diff := externTd.Cmp(localTD); diff > 0 {
|
|
||||||
return true, nil
|
|
||||||
} else if diff < 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
// Local and external difficulty is identical.
|
|
||||||
// Second clause in the if statement reduces the vulnerability to selfish mining.
|
|
||||||
// Please refer to http://www.cs.cornell.edu/~ie53/publications/btcProcFC.pdf
|
|
||||||
reorg := false
|
|
||||||
externNum, localNum := extern.Number.Uint64(), current.Number.Uint64()
|
|
||||||
if externNum < localNum {
|
|
||||||
reorg = true
|
|
||||||
} else if externNum == localNum {
|
|
||||||
var currentPreserve, externPreserve bool
|
|
||||||
if f.preserve != nil {
|
|
||||||
currentPreserve, externPreserve = f.preserve(current), f.preserve(extern)
|
|
||||||
}
|
|
||||||
reorg = !currentPreserve && (externPreserve || f.rand.Float64() < 0.5)
|
|
||||||
}
|
|
||||||
return reorg, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package forkid implements EIP-2124 (https://eips.ethereum.org/EIPS/eip-2124).
|
|
||||||
package forkid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"hash/crc32"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrRemoteStale is returned by the validator if a remote fork checksum is a
|
|
||||||
// subset of our already applied forks, but the announced next fork block is
|
|
||||||
// not on our already passed chain.
|
|
||||||
ErrRemoteStale = errors.New("remote needs update")
|
|
||||||
|
|
||||||
// ErrLocalIncompatibleOrStale is returned by the validator if a remote fork
|
|
||||||
// checksum does not match any local checksum variation, signalling that the
|
|
||||||
// two chains have diverged in the past at some point (possibly at genesis).
|
|
||||||
ErrLocalIncompatibleOrStale = errors.New("local incompatible or needs update")
|
|
||||||
)
|
|
||||||
|
|
||||||
// timestampThreshold is the Ethereum mainnet genesis timestamp. It is used to
|
|
||||||
// differentiate if a forkid.next field is a block number or a timestamp. Whilst
|
|
||||||
// very hacky, something's needed to split the validation during the transition
|
|
||||||
// period (block forks -> time forks).
|
|
||||||
const timestampThreshold = 1438269973
|
|
||||||
|
|
||||||
// Blockchain defines all necessary method to build a forkID.
|
|
||||||
type Blockchain interface {
|
|
||||||
// Config retrieves the chain's fork configuration.
|
|
||||||
Config() *params.ChainConfig
|
|
||||||
|
|
||||||
// Genesis retrieves the chain's genesis block.
|
|
||||||
Genesis() *types.Block
|
|
||||||
|
|
||||||
// CurrentHeader retrieves the current head header of the canonical chain.
|
|
||||||
CurrentHeader() *types.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID is a fork identifier as defined by EIP-2124.
|
|
||||||
type ID struct {
|
|
||||||
Hash [4]byte // CRC32 checksum of the genesis block and passed fork block numbers
|
|
||||||
Next uint64 // Block number of the next upcoming fork, or 0 if no forks are known
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter is a fork id filter to validate a remotely advertised ID.
|
|
||||||
type Filter func(id ID) error
|
|
||||||
|
|
||||||
// NewID calculates the Ethereum fork ID from the chain config, genesis hash, head and time.
|
|
||||||
func NewID(config *params.ChainConfig, genesis *types.Block, head, time uint64) ID {
|
|
||||||
// Calculate the starting checksum from the genesis hash
|
|
||||||
hash := crc32.ChecksumIEEE(genesis.Hash().Bytes())
|
|
||||||
|
|
||||||
// Calculate the current fork checksum and the next fork block
|
|
||||||
forksByBlock, forksByTime := gatherForks(config, genesis.Time())
|
|
||||||
for _, fork := range forksByBlock {
|
|
||||||
if fork <= head {
|
|
||||||
// Fork already passed, checksum the previous hash and the fork number
|
|
||||||
hash = checksumUpdate(hash, fork)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return ID{Hash: checksumToBytes(hash), Next: fork}
|
|
||||||
}
|
|
||||||
for _, fork := range forksByTime {
|
|
||||||
if fork <= time {
|
|
||||||
// Fork already passed, checksum the previous hash and fork timestamp
|
|
||||||
hash = checksumUpdate(hash, fork)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return ID{Hash: checksumToBytes(hash), Next: fork}
|
|
||||||
}
|
|
||||||
return ID{Hash: checksumToBytes(hash), Next: 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIDWithChain calculates the Ethereum fork ID from an existing chain instance.
|
|
||||||
func NewIDWithChain(chain Blockchain) ID {
|
|
||||||
head := chain.CurrentHeader()
|
|
||||||
|
|
||||||
return NewID(
|
|
||||||
chain.Config(),
|
|
||||||
chain.Genesis(),
|
|
||||||
head.Number.Uint64(),
|
|
||||||
head.Time,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFilter creates a filter that returns if a fork ID should be rejected or not
|
|
||||||
// based on the local chain's status.
|
|
||||||
func NewFilter(chain Blockchain) Filter {
|
|
||||||
return newFilter(
|
|
||||||
chain.Config(),
|
|
||||||
chain.Genesis(),
|
|
||||||
func() (uint64, uint64) {
|
|
||||||
head := chain.CurrentHeader()
|
|
||||||
return head.Number.Uint64(), head.Time
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStaticFilter creates a filter at block zero.
|
|
||||||
func NewStaticFilter(config *params.ChainConfig, genesis *types.Block) Filter {
|
|
||||||
head := func() (uint64, uint64) { return 0, 0 }
|
|
||||||
return newFilter(config, genesis, head)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newFilter is the internal version of NewFilter, taking closures as its arguments
|
|
||||||
// instead of a chain. The reason is to allow testing it without having to simulate
|
|
||||||
// an entire blockchain.
|
|
||||||
func newFilter(config *params.ChainConfig, genesis *types.Block, headfn func() (uint64, uint64)) Filter {
|
|
||||||
// Calculate the all the valid fork hash and fork next combos
|
|
||||||
var (
|
|
||||||
forksByBlock, forksByTime = gatherForks(config, genesis.Time())
|
|
||||||
forks = append(append([]uint64{}, forksByBlock...), forksByTime...)
|
|
||||||
sums = make([][4]byte, len(forks)+1) // 0th is the genesis
|
|
||||||
)
|
|
||||||
hash := crc32.ChecksumIEEE(genesis.Hash().Bytes())
|
|
||||||
sums[0] = checksumToBytes(hash)
|
|
||||||
for i, fork := range forks {
|
|
||||||
hash = checksumUpdate(hash, fork)
|
|
||||||
sums[i+1] = checksumToBytes(hash)
|
|
||||||
}
|
|
||||||
// Add two sentries to simplify the fork checks and don't require special
|
|
||||||
// casing the last one.
|
|
||||||
forks = append(forks, math.MaxUint64) // Last fork will never be passed
|
|
||||||
if len(forksByTime) == 0 {
|
|
||||||
// In purely block based forks, avoid the sentry spilling into timestapt territory
|
|
||||||
forksByBlock = append(forksByBlock, math.MaxUint64) // Last fork will never be passed
|
|
||||||
}
|
|
||||||
// Create a validator that will filter out incompatible chains
|
|
||||||
return func(id ID) error {
|
|
||||||
// Run the fork checksum validation ruleset:
|
|
||||||
// 1. If local and remote FORK_CSUM matches, compare local head to FORK_NEXT.
|
|
||||||
// The two nodes are in the same fork state currently. They might know
|
|
||||||
// of differing future forks, but that's not relevant until the fork
|
|
||||||
// triggers (might be postponed, nodes might be updated to match).
|
|
||||||
// 1a. A remotely announced but remotely not passed block is already passed
|
|
||||||
// locally, disconnect, since the chains are incompatible.
|
|
||||||
// 1b. No remotely announced fork; or not yet passed locally, connect.
|
|
||||||
// 2. If the remote FORK_CSUM is a subset of the local past forks and the
|
|
||||||
// remote FORK_NEXT matches with the locally following fork block number,
|
|
||||||
// connect.
|
|
||||||
// Remote node is currently syncing. It might eventually diverge from
|
|
||||||
// us, but at this current point in time we don't have enough information.
|
|
||||||
// 3. If the remote FORK_CSUM is a superset of the local past forks and can
|
|
||||||
// be completed with locally known future forks, connect.
|
|
||||||
// Local node is currently syncing. It might eventually diverge from
|
|
||||||
// the remote, but at this current point in time we don't have enough
|
|
||||||
// information.
|
|
||||||
// 4. Reject in all other cases.
|
|
||||||
block, time := headfn()
|
|
||||||
for i, fork := range forks {
|
|
||||||
// Pick the head comparison based on fork progression
|
|
||||||
head := block
|
|
||||||
if i >= len(forksByBlock) {
|
|
||||||
head = time
|
|
||||||
}
|
|
||||||
// If our head is beyond this fork, continue to the next (we have a dummy
|
|
||||||
// fork of maxuint64 as the last item to always fail this check eventually).
|
|
||||||
if head >= fork {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Found the first unpassed fork block, check if our current state matches
|
|
||||||
// the remote checksum (rule #1).
|
|
||||||
if sums[i] == id.Hash {
|
|
||||||
// Fork checksum matched, check if a remote future fork block already passed
|
|
||||||
// locally without the local node being aware of it (rule #1a).
|
|
||||||
if id.Next > 0 && (head >= id.Next || (id.Next > timestampThreshold && time >= id.Next)) {
|
|
||||||
return ErrLocalIncompatibleOrStale
|
|
||||||
}
|
|
||||||
// Haven't passed locally a remote-only fork, accept the connection (rule #1b).
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// The local and remote nodes are in different forks currently, check if the
|
|
||||||
// remote checksum is a subset of our local forks (rule #2).
|
|
||||||
for j := 0; j < i; j++ {
|
|
||||||
if sums[j] == id.Hash {
|
|
||||||
// Remote checksum is a subset, validate based on the announced next fork
|
|
||||||
if forks[j] != id.Next {
|
|
||||||
return ErrRemoteStale
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remote chain is not a subset of our local one, check if it's a superset by
|
|
||||||
// any chance, signalling that we're simply out of sync (rule #3).
|
|
||||||
for j := i + 1; j < len(sums); j++ {
|
|
||||||
if sums[j] == id.Hash {
|
|
||||||
// Yay, remote checksum is a superset, ignore upcoming forks
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No exact, subset or superset match. We are on differing chains, reject.
|
|
||||||
return ErrLocalIncompatibleOrStale
|
|
||||||
}
|
|
||||||
log.Error("Impossible fork ID validation", "id", id)
|
|
||||||
return nil // Something's very wrong, accept rather than reject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checksumUpdate calculates the next IEEE CRC32 checksum based on the previous
|
|
||||||
// one and a fork block number (equivalent to CRC32(original-blob || fork)).
|
|
||||||
func checksumUpdate(hash uint32, fork uint64) uint32 {
|
|
||||||
var blob [8]byte
|
|
||||||
binary.BigEndian.PutUint64(blob[:], fork)
|
|
||||||
return crc32.Update(hash, crc32.IEEETable, blob[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// checksumToBytes converts a uint32 checksum into a [4]byte array.
|
|
||||||
func checksumToBytes(hash uint32) [4]byte {
|
|
||||||
var blob [4]byte
|
|
||||||
binary.BigEndian.PutUint32(blob[:], hash)
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// gatherForks gathers all the known forks and creates two sorted lists out of
|
|
||||||
// them, one for the block number based forks and the second for the timestamps.
|
|
||||||
func gatherForks(config *params.ChainConfig, genesis uint64) ([]uint64, []uint64) {
|
|
||||||
// Gather all the fork block numbers via reflection
|
|
||||||
kind := reflect.TypeOf(params.ChainConfig{})
|
|
||||||
conf := reflect.ValueOf(config).Elem()
|
|
||||||
x := uint64(0)
|
|
||||||
var (
|
|
||||||
forksByBlock []uint64
|
|
||||||
forksByTime []uint64
|
|
||||||
)
|
|
||||||
for i := 0; i < kind.NumField(); i++ {
|
|
||||||
// Fetch the next field and skip non-fork rules
|
|
||||||
field := kind.Field(i)
|
|
||||||
|
|
||||||
time := strings.HasSuffix(field.Name, "Time")
|
|
||||||
if !time && !strings.HasSuffix(field.Name, "Block") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the fork rule block number or timestamp and aggregate it
|
|
||||||
if field.Type == reflect.TypeOf(&x) {
|
|
||||||
if rule := conf.Field(i).Interface().(*uint64); rule != nil {
|
|
||||||
forksByTime = append(forksByTime, *rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if field.Type == reflect.TypeOf(new(big.Int)) {
|
|
||||||
if rule := conf.Field(i).Interface().(*big.Int); rule != nil {
|
|
||||||
forksByBlock = append(forksByBlock, rule.Uint64())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.Sort(forksByBlock)
|
|
||||||
slices.Sort(forksByTime)
|
|
||||||
|
|
||||||
// Deduplicate fork identifiers applying multiple forks
|
|
||||||
for i := 1; i < len(forksByBlock); i++ {
|
|
||||||
if forksByBlock[i] == forksByBlock[i-1] {
|
|
||||||
forksByBlock = append(forksByBlock[:i], forksByBlock[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 1; i < len(forksByTime); i++ {
|
|
||||||
if forksByTime[i] == forksByTime[i-1] {
|
|
||||||
forksByTime = append(forksByTime[:i], forksByTime[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Skip any forks in block 0, that's the genesis ruleset
|
|
||||||
if len(forksByBlock) > 0 && forksByBlock[0] == 0 {
|
|
||||||
forksByBlock = forksByBlock[1:]
|
|
||||||
}
|
|
||||||
// Skip any forks before genesis.
|
|
||||||
for len(forksByTime) > 0 && forksByTime[0] <= genesis {
|
|
||||||
forksByTime = forksByTime[1:]
|
|
||||||
}
|
|
||||||
return forksByBlock, forksByTime
|
|
||||||
}
|
|
||||||
|
|
@ -1,451 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package forkid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"hash/crc32"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestCreation tests that different genesis and fork rule combinations result in
|
|
||||||
// the correct fork ID.
|
|
||||||
func TestCreation(t *testing.T) {
|
|
||||||
type testcase struct {
|
|
||||||
head uint64
|
|
||||||
time uint64
|
|
||||||
want ID
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
config *params.ChainConfig
|
|
||||||
genesis *types.Block
|
|
||||||
cases []testcase
|
|
||||||
}{
|
|
||||||
// Mainnet test cases
|
|
||||||
{
|
|
||||||
params.MainnetChainConfig,
|
|
||||||
core.DefaultGenesisBlock().ToBlock(),
|
|
||||||
[]testcase{
|
|
||||||
{0, 0, ID{Hash: checksumToBytes(0xfc64ec04), Next: 1150000}}, // Unsynced
|
|
||||||
{1149999, 0, ID{Hash: checksumToBytes(0xfc64ec04), Next: 1150000}}, // Last Frontier block
|
|
||||||
{1150000, 0, ID{Hash: checksumToBytes(0x97c2c34c), Next: 1920000}}, // First Homestead block
|
|
||||||
{1919999, 0, ID{Hash: checksumToBytes(0x97c2c34c), Next: 1920000}}, // Last Homestead block
|
|
||||||
{1920000, 0, ID{Hash: checksumToBytes(0x91d1f948), Next: 2463000}}, // First DAO block
|
|
||||||
{2462999, 0, ID{Hash: checksumToBytes(0x91d1f948), Next: 2463000}}, // Last DAO block
|
|
||||||
{2463000, 0, ID{Hash: checksumToBytes(0x7a64da13), Next: 2675000}}, // First Tangerine block
|
|
||||||
{2674999, 0, ID{Hash: checksumToBytes(0x7a64da13), Next: 2675000}}, // Last Tangerine block
|
|
||||||
{2675000, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 4370000}}, // First Spurious block
|
|
||||||
{4369999, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 4370000}}, // Last Spurious block
|
|
||||||
{4370000, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 7280000}}, // First Byzantium block
|
|
||||||
{7279999, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 7280000}}, // Last Byzantium block
|
|
||||||
{7280000, 0, ID{Hash: checksumToBytes(0x668db0af), Next: 9069000}}, // First and last Constantinople, first Petersburg block
|
|
||||||
{9068999, 0, ID{Hash: checksumToBytes(0x668db0af), Next: 9069000}}, // Last Petersburg block
|
|
||||||
{9069000, 0, ID{Hash: checksumToBytes(0x879d6e30), Next: 9200000}}, // First Istanbul and first Muir Glacier block
|
|
||||||
{9199999, 0, ID{Hash: checksumToBytes(0x879d6e30), Next: 9200000}}, // Last Istanbul and first Muir Glacier block
|
|
||||||
{9200000, 0, ID{Hash: checksumToBytes(0xe029e991), Next: 12244000}}, // First Muir Glacier block
|
|
||||||
{12243999, 0, ID{Hash: checksumToBytes(0xe029e991), Next: 12244000}}, // Last Muir Glacier block
|
|
||||||
{12244000, 0, ID{Hash: checksumToBytes(0x0eb440f6), Next: 12965000}}, // First Berlin block
|
|
||||||
{12964999, 0, ID{Hash: checksumToBytes(0x0eb440f6), Next: 12965000}}, // Last Berlin block
|
|
||||||
{12965000, 0, ID{Hash: checksumToBytes(0xb715077d), Next: 13773000}}, // First London block
|
|
||||||
{13772999, 0, ID{Hash: checksumToBytes(0xb715077d), Next: 13773000}}, // Last London block
|
|
||||||
{13773000, 0, ID{Hash: checksumToBytes(0x20c327fc), Next: 15050000}}, // First Arrow Glacier block
|
|
||||||
{15049999, 0, ID{Hash: checksumToBytes(0x20c327fc), Next: 15050000}}, // Last Arrow Glacier block
|
|
||||||
{15050000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}}, // First Gray Glacier block
|
|
||||||
{20000000, 1681338454, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}}, // Last Gray Glacier block
|
|
||||||
{20000000, 1681338455, ID{Hash: checksumToBytes(0xdce96c2d), Next: 0}}, // First Shanghai block
|
|
||||||
{30000000, 2000000000, ID{Hash: checksumToBytes(0xdce96c2d), Next: 0}}, // Future Shanghai block
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Goerli test cases
|
|
||||||
{
|
|
||||||
params.GoerliChainConfig,
|
|
||||||
core.DefaultGoerliGenesisBlock().ToBlock(),
|
|
||||||
[]testcase{
|
|
||||||
{0, 0, ID{Hash: checksumToBytes(0xa3f5ab08), Next: 1561651}}, // Unsynced, last Frontier, Homestead, Tangerine, Spurious, Byzantium, Constantinople and first Petersburg block
|
|
||||||
{1561650, 0, ID{Hash: checksumToBytes(0xa3f5ab08), Next: 1561651}}, // Last Petersburg block
|
|
||||||
{1561651, 0, ID{Hash: checksumToBytes(0xc25efa5c), Next: 4460644}}, // First Istanbul block
|
|
||||||
{4460643, 0, ID{Hash: checksumToBytes(0xc25efa5c), Next: 4460644}}, // Last Istanbul block
|
|
||||||
{4460644, 0, ID{Hash: checksumToBytes(0x757a1c47), Next: 5062605}}, // First Berlin block
|
|
||||||
{5000000, 0, ID{Hash: checksumToBytes(0x757a1c47), Next: 5062605}}, // Last Berlin block
|
|
||||||
{5062605, 0, ID{Hash: checksumToBytes(0xB8C6299D), Next: 1678832736}}, // First London block
|
|
||||||
{6000000, 1678832735, ID{Hash: checksumToBytes(0xB8C6299D), Next: 1678832736}}, // Last London block
|
|
||||||
{6000001, 1678832736, ID{Hash: checksumToBytes(0xf9843abf), Next: 0}}, // First Shanghai block
|
|
||||||
{6500000, 2678832736, ID{Hash: checksumToBytes(0xf9843abf), Next: 0}}, // Future Shanghai block
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Sepolia test cases
|
|
||||||
{
|
|
||||||
params.SepoliaChainConfig,
|
|
||||||
core.DefaultSepoliaGenesisBlock().ToBlock(),
|
|
||||||
[]testcase{
|
|
||||||
{0, 0, ID{Hash: checksumToBytes(0xfe3366e7), Next: 1735371}}, // Unsynced, last Frontier, Homestead, Tangerine, Spurious, Byzantium, Constantinople, Petersburg, Istanbul, Berlin and first London block
|
|
||||||
{1735370, 0, ID{Hash: checksumToBytes(0xfe3366e7), Next: 1735371}}, // Last London block
|
|
||||||
{1735371, 0, ID{Hash: checksumToBytes(0xb96cbd13), Next: 1677557088}}, // First MergeNetsplit block
|
|
||||||
{1735372, 1677557087, ID{Hash: checksumToBytes(0xb96cbd13), Next: 1677557088}}, // Last MergeNetsplit block
|
|
||||||
{1735372, 1677557088, ID{Hash: checksumToBytes(0xf7f9bc08), Next: 0}}, // First Shanghai block
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Holesky test cases
|
|
||||||
{
|
|
||||||
params.HoleskyChainConfig,
|
|
||||||
core.DefaultHoleskyGenesisBlock().ToBlock(),
|
|
||||||
[]testcase{
|
|
||||||
{0, 0, ID{Hash: checksumToBytes(0xc61a6098), Next: 1696000704}}, // Unsynced, last Frontier, Homestead, Tangerine, Spurious, Byzantium, Constantinople, Petersburg, Istanbul, Berlin, London, Paris block
|
|
||||||
{123, 0, ID{Hash: checksumToBytes(0xc61a6098), Next: 1696000704}}, // First MergeNetsplit block
|
|
||||||
{123, 1696000704, ID{Hash: checksumToBytes(0xfd4f016b), Next: 0}}, // Last MergeNetsplit block
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, tt := range tests {
|
|
||||||
for j, ttt := range tt.cases {
|
|
||||||
if have := NewID(tt.config, tt.genesis, ttt.head, ttt.time); have != ttt.want {
|
|
||||||
t.Errorf("test %d, case %d: fork ID mismatch: have %x, want %x", i, j, have, ttt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidation tests that a local peer correctly validates and accepts a remote
|
|
||||||
// fork ID.
|
|
||||||
func TestValidation(t *testing.T) {
|
|
||||||
// Config that has not timestamp enabled
|
|
||||||
legacyConfig := *params.MainnetChainConfig
|
|
||||||
legacyConfig.ShanghaiTime = nil
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
config *params.ChainConfig
|
|
||||||
head uint64
|
|
||||||
time uint64
|
|
||||||
id ID
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
//------------------
|
|
||||||
// Block based tests
|
|
||||||
//------------------
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, remote announces the same. No future fork is announced.
|
|
||||||
{&legacyConfig, 15050000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, remote announces the same. Remote also announces a next fork
|
|
||||||
// at block 0xffffffff, but that is uncertain.
|
|
||||||
{&legacyConfig, 15050000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: math.MaxUint64}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
|
|
||||||
// also Byzantium, but it's not yet aware of Petersburg (e.g. non updated node before the fork).
|
|
||||||
// In this case we don't know if Petersburg passed yet or not.
|
|
||||||
{&legacyConfig, 7279999, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
|
|
||||||
// also Byzantium, and it's also aware of Petersburg (e.g. updated node before the fork). We
|
|
||||||
// don't know if Petersburg passed yet (will pass) or not.
|
|
||||||
{&legacyConfig, 7279999, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 7280000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Byzantium only (so it's aware of Petersburg), remote announces
|
|
||||||
// also Byzantium, and it's also aware of some random fork (e.g. misconfigured Petersburg). As
|
|
||||||
// neither forks passed at neither nodes, they may mismatch, but we still connect for now.
|
|
||||||
{&legacyConfig, 7279999, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: math.MaxUint64}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet exactly on Petersburg, remote announces Byzantium + knowledge about Petersburg. Remote
|
|
||||||
// is simply out of sync, accept.
|
|
||||||
{&legacyConfig, 7280000, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 7280000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Petersburg, remote announces Byzantium + knowledge about Petersburg. Remote
|
|
||||||
// is simply out of sync, accept.
|
|
||||||
{&legacyConfig, 7987396, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 7280000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Petersburg, remote announces Spurious + knowledge about Byzantium. Remote
|
|
||||||
// is definitely out of sync. It may or may not need the Petersburg update, we don't know yet.
|
|
||||||
{&legacyConfig, 7987396, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 4370000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Byzantium, remote announces Petersburg. Local is out of sync, accept.
|
|
||||||
{&legacyConfig, 7279999, 0, ID{Hash: checksumToBytes(0x668db0af), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Spurious, remote announces Byzantium, but is not aware of Petersburg. Local
|
|
||||||
// out of sync. Local also knows about a future fork, but that is uncertain yet.
|
|
||||||
{&legacyConfig, 4369999, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Petersburg. remote announces Byzantium but is not aware of further forks.
|
|
||||||
// Remote needs software update.
|
|
||||||
{&legacyConfig, 7987396, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 0}, ErrRemoteStale},
|
|
||||||
|
|
||||||
// Local is mainnet Petersburg, and isn't aware of more forks. Remote announces Petersburg +
|
|
||||||
// 0xffffffff. Local needs software update, reject.
|
|
||||||
{&legacyConfig, 7987396, 0, ID{Hash: checksumToBytes(0x5cddc0e1), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Byzantium, and is aware of Petersburg. Remote announces Petersburg +
|
|
||||||
// 0xffffffff. Local needs software update, reject.
|
|
||||||
{&legacyConfig, 7279999, 0, ID{Hash: checksumToBytes(0x5cddc0e1), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Petersburg, remote is Rinkeby Petersburg.
|
|
||||||
{&legacyConfig, 7987396, 0, ID{Hash: checksumToBytes(0xafec6b27), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, far in the future. Remote announces Gopherium (non existing fork)
|
|
||||||
// at some future block 88888888, for itself, but past block for local. Local is incompatible.
|
|
||||||
//
|
|
||||||
// This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
|
|
||||||
//
|
|
||||||
// TODO(karalabe): This testcase will fail once mainnet gets timestamped forks, make legacy chain config
|
|
||||||
{&legacyConfig, 88888888, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 88888888}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Byzantium. Remote is also in Byzantium, but announces Gopherium (non existing
|
|
||||||
// fork) at block 7279999, before Petersburg. Local is incompatible.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): This testcase will fail once mainnet gets timestamped forks, make legacy chain config
|
|
||||||
{&legacyConfig, 7279999, 0, ID{Hash: checksumToBytes(0xa00bc324), Next: 7279999}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
//------------------------------------
|
|
||||||
// Block to timestamp transition tests
|
|
||||||
//------------------------------------
|
|
||||||
|
|
||||||
// Local is mainnet currently in Gray Glacier only (so it's aware of Shanghai), remote announces
|
|
||||||
// also Gray Glacier, but it's not yet aware of Shanghai (e.g. non updated node before the fork).
|
|
||||||
// In this case we don't know if Shanghai passed yet or not.
|
|
||||||
{params.MainnetChainConfig, 15050000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Gray Glacier only (so it's aware of Shanghai), remote announces
|
|
||||||
// also Gray Glacier, and it's also aware of Shanghai (e.g. updated node before the fork). We
|
|
||||||
// don't know if Shanghai passed yet (will pass) or not.
|
|
||||||
{params.MainnetChainConfig, 15050000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Gray Glacier only (so it's aware of Shanghai), remote announces
|
|
||||||
// also Gray Glacier, and it's also aware of some random fork (e.g. misconfigured Shanghai). As
|
|
||||||
// neither forks passed at neither nodes, they may mismatch, but we still connect for now.
|
|
||||||
{params.MainnetChainConfig, 15050000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: math.MaxUint64}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet exactly on Shanghai, remote announces Gray Glacier + knowledge about Shanghai. Remote
|
|
||||||
// is simply out of sync, accept.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote announces Gray Glacier + knowledge about Shanghai. Remote
|
|
||||||
// is simply out of sync, accept.
|
|
||||||
{params.MainnetChainConfig, 20123456, 1681338456, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote announces Arrow Glacier + knowledge about Gray Glacier. Remote
|
|
||||||
// is definitely out of sync. It may or may not need the Shanghai update, we don't know yet.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0x20c327fc), Next: 15050000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, remote announces Shanghai. Local is out of sync, accept.
|
|
||||||
{params.MainnetChainConfig, 15050000, 0, ID{Hash: checksumToBytes(0xdce96c2d), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Arrow Glacier, remote announces Gray Glacier, but is not aware of Shanghai. Local
|
|
||||||
// out of sync. Local also knows about a future fork, but that is uncertain yet.
|
|
||||||
{params.MainnetChainConfig, 13773000, 0, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai. remote announces Gray Glacier but is not aware of further forks.
|
|
||||||
// Remote needs software update.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 0}, ErrRemoteStale},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, and isn't aware of more forks. Remote announces Gray Glacier +
|
|
||||||
// 0xffffffff. Local needs software update, reject.
|
|
||||||
{params.MainnetChainConfig, 15050000, 0, ID{Hash: checksumToBytes(checksumUpdate(0xf0afd0e3, math.MaxUint64)), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, and is aware of Shanghai. Remote announces Shanghai +
|
|
||||||
// 0xffffffff. Local needs software update, reject.
|
|
||||||
{params.MainnetChainConfig, 15050000, 0, ID{Hash: checksumToBytes(checksumUpdate(0xdce96c2d, math.MaxUint64)), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier, far in the future. Remote announces Gopherium (non existing fork)
|
|
||||||
// at some future timestamp 8888888888, for itself, but past block for local. Local is incompatible.
|
|
||||||
//
|
|
||||||
// This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
|
|
||||||
{params.MainnetChainConfig, 888888888, 1660000000, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1660000000}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Gray Glacier. Remote is also in Gray Glacier, but announces Gopherium (non existing
|
|
||||||
// fork) at block 7279999, before Shanghai. Local is incompatible.
|
|
||||||
{params.MainnetChainConfig, 19999999, 1667999999, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1667999999}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
//----------------------
|
|
||||||
// Timestamp based tests
|
|
||||||
//----------------------
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote announces the same. No future fork is announced.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0xdce96c2d), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote announces the same. Remote also announces a next fork
|
|
||||||
// at time 0xffffffff, but that is uncertain.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0xdce96c2d), Next: math.MaxUint64}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Shanghai only (so it's aware of Cancun), remote announces
|
|
||||||
// also Shanghai, but it's not yet aware of Cancun (e.g. non updated node before the fork).
|
|
||||||
// In this case we don't know if Cancun passed yet or not.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced
|
|
||||||
//{params.MainnetChainConfig, 20000000, 1668000000, ID{Hash: checksumToBytes(0x71147644), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Shanghai only (so it's aware of Cancun), remote announces
|
|
||||||
// also Shanghai, and it's also aware of Cancun (e.g. updated node before the fork). We
|
|
||||||
// don't know if Cancun passed yet (will pass) or not.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced and update next timestamp
|
|
||||||
//{params.MainnetChainConfig, 20000000, 1668000000, ID{Hash: checksumToBytes(0x71147644), Next: 1678000000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet currently in Shanghai only (so it's aware of Cancun), remote announces
|
|
||||||
// also Shanghai, and it's also aware of some random fork (e.g. misconfigured Cancun). As
|
|
||||||
// neither forks passed at neither nodes, they may mismatch, but we still connect for now.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced
|
|
||||||
//{params.MainnetChainConfig, 20000000, 1668000000, ID{Hash: checksumToBytes(0x71147644), Next: math.MaxUint64}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet exactly on Cancun, remote announces Shanghai + knowledge about Cancun. Remote
|
|
||||||
// is simply out of sync, accept.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced, update local head and time, next timestamp
|
|
||||||
// {params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0x71147644), Next: 1678000000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Cancun, remote announces Shanghai + knowledge about Cancun. Remote
|
|
||||||
// is simply out of sync, accept.
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced, update local head and time, next timestamp
|
|
||||||
//{params.MainnetChainConfig, 21123456, 1678123456, ID{Hash: checksumToBytes(0x71147644), Next: 1678000000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Prague, remote announces Shanghai + knowledge about Cancun. Remote
|
|
||||||
// is definitely out of sync. It may or may not need the Prague update, we don't know yet.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun **and** Prague is specced, update all the numbers
|
|
||||||
//{params.MainnetChainConfig, 0, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 4370000}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote announces Cancun. Local is out of sync, accept.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced, update remote checksum
|
|
||||||
//{params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0x00000000), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote announces Cancun, but is not aware of Prague. Local
|
|
||||||
// out of sync. Local also knows about a future fork, but that is uncertain yet.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun **and** Prague is specced, update remote checksum
|
|
||||||
//{params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0x00000000), Next: 0}, nil},
|
|
||||||
|
|
||||||
// Local is mainnet Cancun. remote announces Shanghai but is not aware of further forks.
|
|
||||||
// Remote needs software update.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced, update local head and time
|
|
||||||
//{params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0x71147644), Next: 0}, ErrRemoteStale},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, and isn't aware of more forks. Remote announces Shanghai +
|
|
||||||
// 0xffffffff. Local needs software update, reject.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(checksumUpdate(0xdce96c2d, math.MaxUint64)), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, and is aware of Cancun. Remote announces Cancun +
|
|
||||||
// 0xffffffff. Local needs software update, reject.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced, update remote checksum
|
|
||||||
//{params.MainnetChainConfig, 20000000, 1668000000, ID{Hash: checksumToBytes(checksumUpdate(0x00000000, math.MaxUint64)), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, remote is random Shanghai.
|
|
||||||
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0x12345678), Next: 0}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai, far in the future. Remote announces Gopherium (non existing fork)
|
|
||||||
// at some future timestamp 8888888888, for itself, but past block for local. Local is incompatible.
|
|
||||||
//
|
|
||||||
// This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
|
|
||||||
{params.MainnetChainConfig, 88888888, 8888888888, ID{Hash: checksumToBytes(0xdce96c2d), Next: 8888888888}, ErrLocalIncompatibleOrStale},
|
|
||||||
|
|
||||||
// Local is mainnet Shanghai. Remote is also in Shanghai, but announces Gopherium (non existing
|
|
||||||
// fork) at timestamp 1668000000, before Cancun. Local is incompatible.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Enable this when Cancun is specced
|
|
||||||
//{params.MainnetChainConfig, 20999999, 1677999999, ID{Hash: checksumToBytes(0x71147644), Next: 1678000000}, ErrLocalIncompatibleOrStale},
|
|
||||||
}
|
|
||||||
genesis := core.DefaultGenesisBlock().ToBlock()
|
|
||||||
for i, tt := range tests {
|
|
||||||
filter := newFilter(tt.config, genesis, func() (uint64, uint64) { return tt.head, tt.time })
|
|
||||||
if err := filter(tt.id); err != tt.err {
|
|
||||||
t.Errorf("test %d: validation error mismatch: have %v, want %v", i, err, tt.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that IDs are properly RLP encoded (specifically important because we
|
|
||||||
// use uint32 to store the hash, but we need to encode it as [4]byte).
|
|
||||||
func TestEncoding(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
id ID
|
|
||||||
want []byte
|
|
||||||
}{
|
|
||||||
{ID{Hash: checksumToBytes(0), Next: 0}, common.Hex2Bytes("c6840000000080")},
|
|
||||||
{ID{Hash: checksumToBytes(0xdeadbeef), Next: 0xBADDCAFE}, common.Hex2Bytes("ca84deadbeef84baddcafe,")},
|
|
||||||
{ID{Hash: checksumToBytes(math.MaxUint32), Next: math.MaxUint64}, common.Hex2Bytes("ce84ffffffff88ffffffffffffffff")},
|
|
||||||
}
|
|
||||||
for i, tt := range tests {
|
|
||||||
have, err := rlp.EncodeToBytes(tt.id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: failed to encode forkid: %v", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !bytes.Equal(have, tt.want) {
|
|
||||||
t.Errorf("test %d: RLP mismatch: have %x, want %x", i, have, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that time-based forks which are active at genesis are not included in
|
|
||||||
// forkid hash.
|
|
||||||
func TestTimeBasedForkInGenesis(t *testing.T) {
|
|
||||||
var (
|
|
||||||
time = uint64(1690475657)
|
|
||||||
genesis = types.NewBlockWithHeader(&types.Header{Time: time})
|
|
||||||
forkidHash = checksumToBytes(crc32.ChecksumIEEE(genesis.Hash().Bytes()))
|
|
||||||
config = func(shanghai, cancun uint64) *params.ChainConfig {
|
|
||||||
return ¶ms.ChainConfig{
|
|
||||||
ChainID: big.NewInt(1337),
|
|
||||||
HomesteadBlock: big.NewInt(0),
|
|
||||||
DAOForkBlock: nil,
|
|
||||||
DAOForkSupport: true,
|
|
||||||
EIP150Block: big.NewInt(0),
|
|
||||||
EIP155Block: big.NewInt(0),
|
|
||||||
EIP158Block: big.NewInt(0),
|
|
||||||
ByzantiumBlock: big.NewInt(0),
|
|
||||||
ConstantinopleBlock: big.NewInt(0),
|
|
||||||
PetersburgBlock: big.NewInt(0),
|
|
||||||
IstanbulBlock: big.NewInt(0),
|
|
||||||
MuirGlacierBlock: big.NewInt(0),
|
|
||||||
BerlinBlock: big.NewInt(0),
|
|
||||||
LondonBlock: big.NewInt(0),
|
|
||||||
TerminalTotalDifficulty: big.NewInt(0),
|
|
||||||
TerminalTotalDifficultyPassed: true,
|
|
||||||
MergeNetsplitBlock: big.NewInt(0),
|
|
||||||
ShanghaiTime: &shanghai,
|
|
||||||
CancunTime: &cancun,
|
|
||||||
Ethash: new(params.EthashConfig),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tests := []struct {
|
|
||||||
config *params.ChainConfig
|
|
||||||
want ID
|
|
||||||
}{
|
|
||||||
// Shanghai active before genesis, skip
|
|
||||||
{config(time-1, time+1), ID{Hash: forkidHash, Next: time + 1}},
|
|
||||||
|
|
||||||
// Shanghai active at genesis, skip
|
|
||||||
{config(time, time+1), ID{Hash: forkidHash, Next: time + 1}},
|
|
||||||
|
|
||||||
// Shanghai not active, skip
|
|
||||||
{config(time+1, time+2), ID{Hash: forkidHash, Next: time + 1}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
if have := NewID(tt.config, genesis, 0, time); have != tt.want {
|
|
||||||
t.Fatalf("incorrect forkid hash: have %x, want %x", have, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GasPool tracks the amount of gas available during execution of the transactions
|
|
||||||
// in a block. The zero value is a pool with zero gas available.
|
|
||||||
type GasPool uint64
|
|
||||||
|
|
||||||
// AddGas makes gas available for execution.
|
|
||||||
func (gp *GasPool) AddGas(amount uint64) *GasPool {
|
|
||||||
if uint64(*gp) > math.MaxUint64-amount {
|
|
||||||
panic("gas pool pushed above uint64")
|
|
||||||
}
|
|
||||||
*(*uint64)(gp) += amount
|
|
||||||
return gp
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubGas deducts the given amount from the pool if enough gas is
|
|
||||||
// available and returns an error otherwise.
|
|
||||||
func (gp *GasPool) SubGas(amount uint64) error {
|
|
||||||
if uint64(*gp) < amount {
|
|
||||||
return ErrGasLimitReached
|
|
||||||
}
|
|
||||||
*(*uint64)(gp) -= amount
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gas returns the amount of gas remaining in the pool.
|
|
||||||
func (gp *GasPool) Gas() uint64 {
|
|
||||||
return uint64(*gp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGas sets the amount of gas with the provided number.
|
|
||||||
func (gp *GasPool) SetGas(gas uint64) {
|
|
||||||
*(*uint64)(gp) = gas
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gp *GasPool) String() string {
|
|
||||||
return fmt.Sprintf("%d", *gp)
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = (*genesisSpecMarshaling)(nil)
|
|
||||||
|
|
||||||
// MarshalJSON marshals as JSON.
|
|
||||||
func (g Genesis) MarshalJSON() ([]byte, error) {
|
|
||||||
type Genesis struct {
|
|
||||||
Config *params.ChainConfig `json:"config"`
|
|
||||||
Nonce math.HexOrDecimal64 `json:"nonce"`
|
|
||||||
Timestamp math.HexOrDecimal64 `json:"timestamp"`
|
|
||||||
ExtraData hexutil.Bytes `json:"extraData"`
|
|
||||||
GasLimit math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"`
|
|
||||||
Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"`
|
|
||||||
Mixhash common.Hash `json:"mixHash"`
|
|
||||||
Coinbase common.Address `json:"coinbase"`
|
|
||||||
Alloc map[common.UnprefixedAddress]GenesisAccount `json:"alloc" gencodec:"required"`
|
|
||||||
Number math.HexOrDecimal64 `json:"number"`
|
|
||||||
GasUsed math.HexOrDecimal64 `json:"gasUsed"`
|
|
||||||
ParentHash common.Hash `json:"parentHash"`
|
|
||||||
BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"`
|
|
||||||
ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"`
|
|
||||||
BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"`
|
|
||||||
}
|
|
||||||
var enc Genesis
|
|
||||||
enc.Config = g.Config
|
|
||||||
enc.Nonce = math.HexOrDecimal64(g.Nonce)
|
|
||||||
enc.Timestamp = math.HexOrDecimal64(g.Timestamp)
|
|
||||||
enc.ExtraData = g.ExtraData
|
|
||||||
enc.GasLimit = math.HexOrDecimal64(g.GasLimit)
|
|
||||||
enc.Difficulty = (*math.HexOrDecimal256)(g.Difficulty)
|
|
||||||
enc.Mixhash = g.Mixhash
|
|
||||||
enc.Coinbase = g.Coinbase
|
|
||||||
if g.Alloc != nil {
|
|
||||||
enc.Alloc = make(map[common.UnprefixedAddress]GenesisAccount, len(g.Alloc))
|
|
||||||
for k, v := range g.Alloc {
|
|
||||||
enc.Alloc[common.UnprefixedAddress(k)] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enc.Number = math.HexOrDecimal64(g.Number)
|
|
||||||
enc.GasUsed = math.HexOrDecimal64(g.GasUsed)
|
|
||||||
enc.ParentHash = g.ParentHash
|
|
||||||
enc.BaseFee = (*math.HexOrDecimal256)(g.BaseFee)
|
|
||||||
enc.ExcessBlobGas = (*math.HexOrDecimal64)(g.ExcessBlobGas)
|
|
||||||
enc.BlobGasUsed = (*math.HexOrDecimal64)(g.BlobGasUsed)
|
|
||||||
return json.Marshal(&enc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals from JSON.
|
|
||||||
func (g *Genesis) UnmarshalJSON(input []byte) error {
|
|
||||||
type Genesis struct {
|
|
||||||
Config *params.ChainConfig `json:"config"`
|
|
||||||
Nonce *math.HexOrDecimal64 `json:"nonce"`
|
|
||||||
Timestamp *math.HexOrDecimal64 `json:"timestamp"`
|
|
||||||
ExtraData *hexutil.Bytes `json:"extraData"`
|
|
||||||
GasLimit *math.HexOrDecimal64 `json:"gasLimit" gencodec:"required"`
|
|
||||||
Difficulty *math.HexOrDecimal256 `json:"difficulty" gencodec:"required"`
|
|
||||||
Mixhash *common.Hash `json:"mixHash"`
|
|
||||||
Coinbase *common.Address `json:"coinbase"`
|
|
||||||
Alloc map[common.UnprefixedAddress]GenesisAccount `json:"alloc" gencodec:"required"`
|
|
||||||
Number *math.HexOrDecimal64 `json:"number"`
|
|
||||||
GasUsed *math.HexOrDecimal64 `json:"gasUsed"`
|
|
||||||
ParentHash *common.Hash `json:"parentHash"`
|
|
||||||
BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"`
|
|
||||||
ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"`
|
|
||||||
BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"`
|
|
||||||
}
|
|
||||||
var dec Genesis
|
|
||||||
if err := json.Unmarshal(input, &dec); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if dec.Config != nil {
|
|
||||||
g.Config = dec.Config
|
|
||||||
}
|
|
||||||
if dec.Nonce != nil {
|
|
||||||
g.Nonce = uint64(*dec.Nonce)
|
|
||||||
}
|
|
||||||
if dec.Timestamp != nil {
|
|
||||||
g.Timestamp = uint64(*dec.Timestamp)
|
|
||||||
}
|
|
||||||
if dec.ExtraData != nil {
|
|
||||||
g.ExtraData = *dec.ExtraData
|
|
||||||
}
|
|
||||||
if dec.GasLimit == nil {
|
|
||||||
return errors.New("missing required field 'gasLimit' for Genesis")
|
|
||||||
}
|
|
||||||
g.GasLimit = uint64(*dec.GasLimit)
|
|
||||||
if dec.Difficulty == nil {
|
|
||||||
return errors.New("missing required field 'difficulty' for Genesis")
|
|
||||||
}
|
|
||||||
g.Difficulty = (*big.Int)(dec.Difficulty)
|
|
||||||
if dec.Mixhash != nil {
|
|
||||||
g.Mixhash = *dec.Mixhash
|
|
||||||
}
|
|
||||||
if dec.Coinbase != nil {
|
|
||||||
g.Coinbase = *dec.Coinbase
|
|
||||||
}
|
|
||||||
if dec.Alloc == nil {
|
|
||||||
return errors.New("missing required field 'alloc' for Genesis")
|
|
||||||
}
|
|
||||||
g.Alloc = make(GenesisAlloc, len(dec.Alloc))
|
|
||||||
for k, v := range dec.Alloc {
|
|
||||||
g.Alloc[common.Address(k)] = v
|
|
||||||
}
|
|
||||||
if dec.Number != nil {
|
|
||||||
g.Number = uint64(*dec.Number)
|
|
||||||
}
|
|
||||||
if dec.GasUsed != nil {
|
|
||||||
g.GasUsed = uint64(*dec.GasUsed)
|
|
||||||
}
|
|
||||||
if dec.ParentHash != nil {
|
|
||||||
g.ParentHash = *dec.ParentHash
|
|
||||||
}
|
|
||||||
if dec.BaseFee != nil {
|
|
||||||
g.BaseFee = (*big.Int)(dec.BaseFee)
|
|
||||||
}
|
|
||||||
if dec.ExcessBlobGas != nil {
|
|
||||||
g.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas)
|
|
||||||
}
|
|
||||||
if dec.BlobGasUsed != nil {
|
|
||||||
g.BlobGasUsed = (*uint64)(dec.BlobGasUsed)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = (*genesisAccountMarshaling)(nil)
|
|
||||||
|
|
||||||
// MarshalJSON marshals as JSON.
|
|
||||||
func (g GenesisAccount) MarshalJSON() ([]byte, error) {
|
|
||||||
type GenesisAccount struct {
|
|
||||||
Code hexutil.Bytes `json:"code,omitempty"`
|
|
||||||
Storage map[storageJSON]storageJSON `json:"storage,omitempty"`
|
|
||||||
Balance *math.HexOrDecimal256 `json:"balance" gencodec:"required"`
|
|
||||||
Nonce math.HexOrDecimal64 `json:"nonce,omitempty"`
|
|
||||||
PrivateKey hexutil.Bytes `json:"secretKey,omitempty"`
|
|
||||||
}
|
|
||||||
var enc GenesisAccount
|
|
||||||
enc.Code = g.Code
|
|
||||||
if g.Storage != nil {
|
|
||||||
enc.Storage = make(map[storageJSON]storageJSON, len(g.Storage))
|
|
||||||
for k, v := range g.Storage {
|
|
||||||
enc.Storage[storageJSON(k)] = storageJSON(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enc.Balance = (*math.HexOrDecimal256)(g.Balance)
|
|
||||||
enc.Nonce = math.HexOrDecimal64(g.Nonce)
|
|
||||||
enc.PrivateKey = g.PrivateKey
|
|
||||||
return json.Marshal(&enc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals from JSON.
|
|
||||||
func (g *GenesisAccount) UnmarshalJSON(input []byte) error {
|
|
||||||
type GenesisAccount struct {
|
|
||||||
Code *hexutil.Bytes `json:"code,omitempty"`
|
|
||||||
Storage map[storageJSON]storageJSON `json:"storage,omitempty"`
|
|
||||||
Balance *math.HexOrDecimal256 `json:"balance" gencodec:"required"`
|
|
||||||
Nonce *math.HexOrDecimal64 `json:"nonce,omitempty"`
|
|
||||||
PrivateKey *hexutil.Bytes `json:"secretKey,omitempty"`
|
|
||||||
}
|
|
||||||
var dec GenesisAccount
|
|
||||||
if err := json.Unmarshal(input, &dec); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if dec.Code != nil {
|
|
||||||
g.Code = *dec.Code
|
|
||||||
}
|
|
||||||
if dec.Storage != nil {
|
|
||||||
g.Storage = make(map[common.Hash]common.Hash, len(dec.Storage))
|
|
||||||
for k, v := range dec.Storage {
|
|
||||||
g.Storage[common.Hash(k)] = common.Hash(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dec.Balance == nil {
|
|
||||||
return errors.New("missing required field 'balance' for GenesisAccount")
|
|
||||||
}
|
|
||||||
g.Balance = (*big.Int)(dec.Balance)
|
|
||||||
if dec.Nonce != nil {
|
|
||||||
g.Nonce = uint64(*dec.Nonce)
|
|
||||||
}
|
|
||||||
if dec.PrivateKey != nil {
|
|
||||||
g.PrivateKey = *dec.PrivateKey
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
642
core/genesis.go
642
core/genesis.go
|
|
@ -1,642 +0,0 @@
|
||||||
// Copyright 2014 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"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/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/triedb/pathdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate go run github.com/fjl/gencodec -type Genesis -field-override genesisSpecMarshaling -out gen_genesis.go
|
|
||||||
//go:generate go run github.com/fjl/gencodec -type GenesisAccount -field-override genesisAccountMarshaling -out gen_genesis_account.go
|
|
||||||
|
|
||||||
var errGenesisNoConfig = errors.New("genesis has no chain configuration")
|
|
||||||
|
|
||||||
// Genesis specifies the header fields, state of a genesis block. It also defines hard
|
|
||||||
// fork switch-over blocks through the chain configuration.
|
|
||||||
type Genesis struct {
|
|
||||||
Config *params.ChainConfig `json:"config"`
|
|
||||||
Nonce uint64 `json:"nonce"`
|
|
||||||
Timestamp uint64 `json:"timestamp"`
|
|
||||||
ExtraData []byte `json:"extraData"`
|
|
||||||
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
|
|
||||||
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
|
|
||||||
Mixhash common.Hash `json:"mixHash"`
|
|
||||||
Coinbase common.Address `json:"coinbase"`
|
|
||||||
Alloc GenesisAlloc `json:"alloc" gencodec:"required"`
|
|
||||||
|
|
||||||
// These fields are used for consensus tests. Please don't use them
|
|
||||||
// in actual genesis blocks.
|
|
||||||
Number uint64 `json:"number"`
|
|
||||||
GasUsed uint64 `json:"gasUsed"`
|
|
||||||
ParentHash common.Hash `json:"parentHash"`
|
|
||||||
BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559
|
|
||||||
ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844
|
|
||||||
BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadGenesis(db ethdb.Database) (*Genesis, error) {
|
|
||||||
var genesis Genesis
|
|
||||||
stored := rawdb.ReadCanonicalHash(db, 0)
|
|
||||||
if (stored == common.Hash{}) {
|
|
||||||
return nil, fmt.Errorf("invalid genesis hash in database: %x", stored)
|
|
||||||
}
|
|
||||||
blob := rawdb.ReadGenesisStateSpec(db, stored)
|
|
||||||
if blob == nil {
|
|
||||||
return nil, errors.New("genesis state missing from db")
|
|
||||||
}
|
|
||||||
if len(blob) != 0 {
|
|
||||||
if err := genesis.Alloc.UnmarshalJSON(blob); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not unmarshal genesis state json: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
genesis.Config = rawdb.ReadChainConfig(db, stored)
|
|
||||||
if genesis.Config == nil {
|
|
||||||
return nil, errors.New("genesis config missing from db")
|
|
||||||
}
|
|
||||||
genesisBlock := rawdb.ReadBlock(db, stored, 0)
|
|
||||||
if genesisBlock == nil {
|
|
||||||
return nil, errors.New("genesis block missing from db")
|
|
||||||
}
|
|
||||||
genesisHeader := genesisBlock.Header()
|
|
||||||
genesis.Nonce = genesisHeader.Nonce.Uint64()
|
|
||||||
genesis.Timestamp = genesisHeader.Time
|
|
||||||
genesis.ExtraData = genesisHeader.Extra
|
|
||||||
genesis.GasLimit = genesisHeader.GasLimit
|
|
||||||
genesis.Difficulty = genesisHeader.Difficulty
|
|
||||||
genesis.Mixhash = genesisHeader.MixDigest
|
|
||||||
genesis.Coinbase = genesisHeader.Coinbase
|
|
||||||
genesis.BaseFee = genesisHeader.BaseFee
|
|
||||||
genesis.ExcessBlobGas = genesisHeader.ExcessBlobGas
|
|
||||||
genesis.BlobGasUsed = genesisHeader.BlobGasUsed
|
|
||||||
|
|
||||||
return &genesis, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenesisAlloc specifies the initial state that is part of the genesis block.
|
|
||||||
type GenesisAlloc map[common.Address]GenesisAccount
|
|
||||||
|
|
||||||
func (ga *GenesisAlloc) UnmarshalJSON(data []byte) error {
|
|
||||||
m := make(map[common.UnprefixedAddress]GenesisAccount)
|
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*ga = make(GenesisAlloc)
|
|
||||||
for addr, a := range m {
|
|
||||||
(*ga)[common.Address(addr)] = a
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// hash computes the state root according to the genesis specification.
|
|
||||||
func (ga *GenesisAlloc) hash(isVerkle bool) (common.Hash, error) {
|
|
||||||
// If a genesis-time verkle trie is requested, create a trie config
|
|
||||||
// with the verkle trie enabled so that the tree can be initialized
|
|
||||||
// as such.
|
|
||||||
var config *trie.Config
|
|
||||||
if isVerkle {
|
|
||||||
config = &trie.Config{
|
|
||||||
PathDB: pathdb.Defaults,
|
|
||||||
IsVerkle: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create an ephemeral in-memory database for computing hash,
|
|
||||||
// all the derived states will be discarded to not pollute disk.
|
|
||||||
db := state.NewDatabaseWithConfig(rawdb.NewMemoryDatabase(), config)
|
|
||||||
statedb, err := state.New(types.EmptyRootHash, db, nil)
|
|
||||||
if err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
for addr, account := range *ga {
|
|
||||||
if account.Balance != nil {
|
|
||||||
statedb.AddBalance(addr, account.Balance)
|
|
||||||
}
|
|
||||||
statedb.SetCode(addr, account.Code)
|
|
||||||
statedb.SetNonce(addr, account.Nonce)
|
|
||||||
for key, value := range account.Storage {
|
|
||||||
statedb.SetState(addr, key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return statedb.Commit(0, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// flush is very similar with hash, but the main difference is all the generated
|
|
||||||
// states will be persisted into the given database. Also, the genesis state
|
|
||||||
// specification will be flushed as well.
|
|
||||||
func (ga *GenesisAlloc) flush(db ethdb.Database, triedb *trie.Database, blockhash common.Hash) error {
|
|
||||||
statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseWithNodeDB(db, triedb), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for addr, account := range *ga {
|
|
||||||
if account.Balance != nil {
|
|
||||||
statedb.AddBalance(addr, account.Balance)
|
|
||||||
}
|
|
||||||
statedb.SetCode(addr, account.Code)
|
|
||||||
statedb.SetNonce(addr, account.Nonce)
|
|
||||||
for key, value := range account.Storage {
|
|
||||||
statedb.SetState(addr, key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root, err := statedb.Commit(0, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Commit newly generated states into disk if it's not empty.
|
|
||||||
if root != types.EmptyRootHash {
|
|
||||||
if err := triedb.Commit(root, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Marshal the genesis state specification and persist.
|
|
||||||
blob, err := json.Marshal(ga)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rawdb.WriteGenesisStateSpec(db, blockhash, blob)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenesisAccount is an account in the state of the genesis block.
|
|
||||||
type GenesisAccount struct {
|
|
||||||
Code []byte `json:"code,omitempty"`
|
|
||||||
Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
|
|
||||||
Balance *big.Int `json:"balance" gencodec:"required"`
|
|
||||||
Nonce uint64 `json:"nonce,omitempty"`
|
|
||||||
PrivateKey []byte `json:"secretKey,omitempty"` // for tests
|
|
||||||
}
|
|
||||||
|
|
||||||
// field type overrides for gencodec
|
|
||||||
type genesisSpecMarshaling struct {
|
|
||||||
Nonce math.HexOrDecimal64
|
|
||||||
Timestamp math.HexOrDecimal64
|
|
||||||
ExtraData hexutil.Bytes
|
|
||||||
GasLimit math.HexOrDecimal64
|
|
||||||
GasUsed math.HexOrDecimal64
|
|
||||||
Number math.HexOrDecimal64
|
|
||||||
Difficulty *math.HexOrDecimal256
|
|
||||||
Alloc map[common.UnprefixedAddress]GenesisAccount
|
|
||||||
BaseFee *math.HexOrDecimal256
|
|
||||||
ExcessBlobGas *math.HexOrDecimal64
|
|
||||||
BlobGasUsed *math.HexOrDecimal64
|
|
||||||
}
|
|
||||||
|
|
||||||
type genesisAccountMarshaling struct {
|
|
||||||
Code hexutil.Bytes
|
|
||||||
Balance *math.HexOrDecimal256
|
|
||||||
Nonce math.HexOrDecimal64
|
|
||||||
Storage map[storageJSON]storageJSON
|
|
||||||
PrivateKey hexutil.Bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// storageJSON represents a 256 bit byte array, but allows less than 256 bits when
|
|
||||||
// unmarshaling from hex.
|
|
||||||
type storageJSON common.Hash
|
|
||||||
|
|
||||||
func (h *storageJSON) UnmarshalText(text []byte) error {
|
|
||||||
text = bytes.TrimPrefix(text, []byte("0x"))
|
|
||||||
if len(text) > 64 {
|
|
||||||
return fmt.Errorf("too many hex characters in storage key/value %q", text)
|
|
||||||
}
|
|
||||||
offset := len(h) - len(text)/2 // pad on the left
|
|
||||||
if _, err := hex.Decode(h[offset:], text); err != nil {
|
|
||||||
return fmt.Errorf("invalid hex storage key/value %q", text)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h storageJSON) MarshalText() ([]byte, error) {
|
|
||||||
return hexutil.Bytes(h[:]).MarshalText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenesisMismatchError is raised when trying to overwrite an existing
|
|
||||||
// genesis block with an incompatible one.
|
|
||||||
type GenesisMismatchError struct {
|
|
||||||
Stored, New common.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *GenesisMismatchError) Error() string {
|
|
||||||
return fmt.Sprintf("database contains incompatible genesis (have %x, new %x)", e.Stored, e.New)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainOverrides contains the changes to chain config.
|
|
||||||
type ChainOverrides struct {
|
|
||||||
OverrideCancun *uint64
|
|
||||||
OverrideVerkle *uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupGenesisBlock writes or updates the genesis block in db.
|
|
||||||
// The block that will be used is:
|
|
||||||
//
|
|
||||||
// genesis == nil genesis != nil
|
|
||||||
// +------------------------------------------
|
|
||||||
// db has no genesis | main-net default | genesis
|
|
||||||
// db has genesis | from DB | genesis (if compatible)
|
|
||||||
//
|
|
||||||
// The stored chain configuration will be updated if it is compatible (i.e. does not
|
|
||||||
// specify a fork block below the local head block). In case of a conflict, the
|
|
||||||
// error is a *params.ConfigCompatError and the new, unwritten config is returned.
|
|
||||||
//
|
|
||||||
// The returned chain configuration is never nil.
|
|
||||||
func SetupGenesisBlock(db ethdb.Database, triedb *trie.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
return SetupGenesisBlockWithOverride(db, triedb, genesis, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *trie.Database, genesis *Genesis, overrides *ChainOverrides) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
if genesis != nil && genesis.Config == nil {
|
|
||||||
return params.AllEthashProtocolChanges, common.Hash{}, errGenesisNoConfig
|
|
||||||
}
|
|
||||||
applyOverrides := func(config *params.ChainConfig) {
|
|
||||||
if config != nil {
|
|
||||||
if overrides != nil && overrides.OverrideCancun != nil {
|
|
||||||
config.CancunTime = overrides.OverrideCancun
|
|
||||||
}
|
|
||||||
if overrides != nil && overrides.OverrideVerkle != nil {
|
|
||||||
config.VerkleTime = overrides.OverrideVerkle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Just commit the new block if there is no stored genesis block.
|
|
||||||
stored := rawdb.ReadCanonicalHash(db, 0)
|
|
||||||
if (stored == common.Hash{}) {
|
|
||||||
if genesis == nil {
|
|
||||||
log.Info("Writing default main-net genesis block")
|
|
||||||
genesis = DefaultGenesisBlock()
|
|
||||||
} else {
|
|
||||||
log.Info("Writing custom genesis block")
|
|
||||||
}
|
|
||||||
applyOverrides(genesis.Config)
|
|
||||||
block, err := genesis.Commit(db, triedb)
|
|
||||||
if err != nil {
|
|
||||||
return genesis.Config, common.Hash{}, err
|
|
||||||
}
|
|
||||||
return genesis.Config, block.Hash(), nil
|
|
||||||
}
|
|
||||||
// The genesis block is present(perhaps in ancient database) while the
|
|
||||||
// state database is not initialized yet. It can happen that the node
|
|
||||||
// is initialized with an external ancient store. Commit genesis state
|
|
||||||
// in this case.
|
|
||||||
header := rawdb.ReadHeader(db, stored, 0)
|
|
||||||
if header.Root != types.EmptyRootHash && !triedb.Initialized(header.Root) {
|
|
||||||
if genesis == nil {
|
|
||||||
genesis = DefaultGenesisBlock()
|
|
||||||
}
|
|
||||||
applyOverrides(genesis.Config)
|
|
||||||
// Ensure the stored genesis matches with the given one.
|
|
||||||
hash := genesis.ToBlock().Hash()
|
|
||||||
if hash != stored {
|
|
||||||
return genesis.Config, hash, &GenesisMismatchError{stored, hash}
|
|
||||||
}
|
|
||||||
block, err := genesis.Commit(db, triedb)
|
|
||||||
if err != nil {
|
|
||||||
return genesis.Config, hash, err
|
|
||||||
}
|
|
||||||
return genesis.Config, block.Hash(), nil
|
|
||||||
}
|
|
||||||
// Check whether the genesis block is already written.
|
|
||||||
if genesis != nil {
|
|
||||||
applyOverrides(genesis.Config)
|
|
||||||
hash := genesis.ToBlock().Hash()
|
|
||||||
if hash != stored {
|
|
||||||
return genesis.Config, hash, &GenesisMismatchError{stored, hash}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Get the existing chain configuration.
|
|
||||||
newcfg := genesis.configOrDefault(stored)
|
|
||||||
applyOverrides(newcfg)
|
|
||||||
if err := newcfg.CheckConfigForkOrder(); err != nil {
|
|
||||||
return newcfg, common.Hash{}, err
|
|
||||||
}
|
|
||||||
storedcfg := rawdb.ReadChainConfig(db, stored)
|
|
||||||
if storedcfg == nil {
|
|
||||||
log.Warn("Found genesis block without chain config")
|
|
||||||
rawdb.WriteChainConfig(db, stored, newcfg)
|
|
||||||
return newcfg, stored, nil
|
|
||||||
}
|
|
||||||
storedData, _ := json.Marshal(storedcfg)
|
|
||||||
// Special case: if a private network is being used (no genesis and also no
|
|
||||||
// mainnet hash in the database), we must not apply the `configOrDefault`
|
|
||||||
// chain config as that would be AllProtocolChanges (applying any new fork
|
|
||||||
// on top of an existing private network genesis block). In that case, only
|
|
||||||
// apply the overrides.
|
|
||||||
if genesis == nil && stored != params.MainnetGenesisHash {
|
|
||||||
newcfg = storedcfg
|
|
||||||
applyOverrides(newcfg)
|
|
||||||
}
|
|
||||||
// Check config compatibility and write the config. Compatibility errors
|
|
||||||
// are returned to the caller unless we're already at block zero.
|
|
||||||
head := rawdb.ReadHeadHeader(db)
|
|
||||||
if head == nil {
|
|
||||||
return newcfg, stored, errors.New("missing head header")
|
|
||||||
}
|
|
||||||
compatErr := storedcfg.CheckCompatible(newcfg, head.Number.Uint64(), head.Time)
|
|
||||||
if compatErr != nil && ((head.Number.Uint64() != 0 && compatErr.RewindToBlock != 0) || (head.Time != 0 && compatErr.RewindToTime != 0)) {
|
|
||||||
return newcfg, stored, compatErr
|
|
||||||
}
|
|
||||||
// Don't overwrite if the old is identical to the new
|
|
||||||
if newData, _ := json.Marshal(newcfg); !bytes.Equal(storedData, newData) {
|
|
||||||
rawdb.WriteChainConfig(db, stored, newcfg)
|
|
||||||
}
|
|
||||||
return newcfg, stored, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadChainConfig loads the stored chain config if it is already present in
|
|
||||||
// database, otherwise, return the config in the provided genesis specification.
|
|
||||||
func LoadChainConfig(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, error) {
|
|
||||||
// Load the stored chain config from the database. It can be nil
|
|
||||||
// in case the database is empty. Notably, we only care about the
|
|
||||||
// chain config corresponds to the canonical chain.
|
|
||||||
stored := rawdb.ReadCanonicalHash(db, 0)
|
|
||||||
if stored != (common.Hash{}) {
|
|
||||||
storedcfg := rawdb.ReadChainConfig(db, stored)
|
|
||||||
if storedcfg != nil {
|
|
||||||
return storedcfg, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Load the config from the provided genesis specification
|
|
||||||
if genesis != nil {
|
|
||||||
// Reject invalid genesis spec without valid chain config
|
|
||||||
if genesis.Config == nil {
|
|
||||||
return nil, errGenesisNoConfig
|
|
||||||
}
|
|
||||||
// If the canonical genesis header is present, but the chain
|
|
||||||
// config is missing(initialize the empty leveldb with an
|
|
||||||
// external ancient chain segment), ensure the provided genesis
|
|
||||||
// is matched.
|
|
||||||
if stored != (common.Hash{}) && genesis.ToBlock().Hash() != stored {
|
|
||||||
return nil, &GenesisMismatchError{stored, genesis.ToBlock().Hash()}
|
|
||||||
}
|
|
||||||
return genesis.Config, nil
|
|
||||||
}
|
|
||||||
// There is no stored chain config and no new config provided,
|
|
||||||
// In this case the default chain config(mainnet) will be used
|
|
||||||
return params.MainnetChainConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Genesis) configOrDefault(ghash common.Hash) *params.ChainConfig {
|
|
||||||
switch {
|
|
||||||
case g != nil:
|
|
||||||
return g.Config
|
|
||||||
case ghash == params.MainnetGenesisHash:
|
|
||||||
return params.MainnetChainConfig
|
|
||||||
case ghash == params.SepoliaGenesisHash:
|
|
||||||
return params.SepoliaChainConfig
|
|
||||||
case ghash == params.GoerliGenesisHash:
|
|
||||||
return params.GoerliChainConfig
|
|
||||||
default:
|
|
||||||
return params.AllEthashProtocolChanges
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsVerkle indicates whether the state is already stored in a verkle
|
|
||||||
// tree at genesis time.
|
|
||||||
func (g *Genesis) IsVerkle() bool {
|
|
||||||
return g.Config.IsVerkle(new(big.Int).SetUint64(g.Number), g.Timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBlock returns the genesis block according to genesis specification.
|
|
||||||
func (g *Genesis) ToBlock() *types.Block {
|
|
||||||
root, err := g.Alloc.hash(g.IsVerkle())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
head := &types.Header{
|
|
||||||
Number: new(big.Int).SetUint64(g.Number),
|
|
||||||
Nonce: types.EncodeNonce(g.Nonce),
|
|
||||||
Time: g.Timestamp,
|
|
||||||
ParentHash: g.ParentHash,
|
|
||||||
Extra: g.ExtraData,
|
|
||||||
GasLimit: g.GasLimit,
|
|
||||||
GasUsed: g.GasUsed,
|
|
||||||
BaseFee: g.BaseFee,
|
|
||||||
Difficulty: g.Difficulty,
|
|
||||||
MixDigest: g.Mixhash,
|
|
||||||
Coinbase: g.Coinbase,
|
|
||||||
Root: root,
|
|
||||||
}
|
|
||||||
if g.GasLimit == 0 {
|
|
||||||
head.GasLimit = params.GenesisGasLimit
|
|
||||||
}
|
|
||||||
if g.Difficulty == nil && g.Mixhash == (common.Hash{}) {
|
|
||||||
head.Difficulty = params.GenesisDifficulty
|
|
||||||
}
|
|
||||||
if g.Config != nil && g.Config.IsLondon(common.Big0) {
|
|
||||||
if g.BaseFee != nil {
|
|
||||||
head.BaseFee = g.BaseFee
|
|
||||||
} else {
|
|
||||||
head.BaseFee = new(big.Int).SetUint64(params.InitialBaseFee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var withdrawals []*types.Withdrawal
|
|
||||||
if conf := g.Config; conf != nil {
|
|
||||||
num := big.NewInt(int64(g.Number))
|
|
||||||
if conf.IsShanghai(num, g.Timestamp) {
|
|
||||||
head.WithdrawalsHash = &types.EmptyWithdrawalsHash
|
|
||||||
withdrawals = make([]*types.Withdrawal, 0)
|
|
||||||
}
|
|
||||||
if conf.IsCancun(num, g.Timestamp) {
|
|
||||||
// EIP-4788: The parentBeaconBlockRoot of the genesis block is always
|
|
||||||
// the zero hash. This is because the genesis block does not have a parent
|
|
||||||
// by definition.
|
|
||||||
head.ParentBeaconRoot = new(common.Hash)
|
|
||||||
// EIP-4844 fields
|
|
||||||
head.ExcessBlobGas = g.ExcessBlobGas
|
|
||||||
head.BlobGasUsed = g.BlobGasUsed
|
|
||||||
if head.ExcessBlobGas == nil {
|
|
||||||
head.ExcessBlobGas = new(uint64)
|
|
||||||
}
|
|
||||||
if head.BlobGasUsed == nil {
|
|
||||||
head.BlobGasUsed = new(uint64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return types.NewBlock(head, nil, nil, nil, trie.NewStackTrie(nil)).WithWithdrawals(withdrawals)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit writes the block and state of a genesis specification to the database.
|
|
||||||
// The block is committed as the canonical head block.
|
|
||||||
func (g *Genesis) Commit(db ethdb.Database, triedb *trie.Database) (*types.Block, error) {
|
|
||||||
block := g.ToBlock()
|
|
||||||
if block.Number().Sign() != 0 {
|
|
||||||
return nil, errors.New("can't commit genesis block with number > 0")
|
|
||||||
}
|
|
||||||
config := g.Config
|
|
||||||
if config == nil {
|
|
||||||
config = params.AllEthashProtocolChanges
|
|
||||||
}
|
|
||||||
if err := config.CheckConfigForkOrder(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if config.Clique != nil && len(block.Extra()) < 32+crypto.SignatureLength {
|
|
||||||
return nil, errors.New("can't start clique chain without signers")
|
|
||||||
}
|
|
||||||
// All the checks has passed, flush the states derived from the genesis
|
|
||||||
// specification as well as the specification itself into the provided
|
|
||||||
// database.
|
|
||||||
if err := g.Alloc.flush(db, triedb, block.Hash()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawdb.WriteTd(db, block.Hash(), block.NumberU64(), block.Difficulty())
|
|
||||||
rawdb.WriteBlock(db, block)
|
|
||||||
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())
|
|
||||||
rawdb.WriteChainConfig(db, block.Hash(), config)
|
|
||||||
return block, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustCommit writes the genesis block and state to db, panicking on error.
|
|
||||||
// The block is committed as the canonical head block.
|
|
||||||
func (g *Genesis) MustCommit(db ethdb.Database, triedb *trie.Database) *types.Block {
|
|
||||||
block, err := g.Commit(db, triedb)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultGenesisBlock returns the Ethereum main net genesis block.
|
|
||||||
func DefaultGenesisBlock() *Genesis {
|
|
||||||
return &Genesis{
|
|
||||||
Config: params.MainnetChainConfig,
|
|
||||||
Nonce: 66,
|
|
||||||
ExtraData: hexutil.MustDecode("0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"),
|
|
||||||
GasLimit: 5000,
|
|
||||||
Difficulty: big.NewInt(17179869184),
|
|
||||||
Alloc: decodePrealloc(mainnetAllocData),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultGoerliGenesisBlock returns the Görli network genesis block.
|
|
||||||
func DefaultGoerliGenesisBlock() *Genesis {
|
|
||||||
return &Genesis{
|
|
||||||
Config: params.GoerliChainConfig,
|
|
||||||
Timestamp: 1548854791,
|
|
||||||
ExtraData: hexutil.MustDecode("0x22466c6578692069732061207468696e6722202d204166726900000000000000e0a2bd4258d2768837baa26a28fe71dc079f84c70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"),
|
|
||||||
GasLimit: 10485760,
|
|
||||||
Difficulty: big.NewInt(1),
|
|
||||||
Alloc: decodePrealloc(goerliAllocData),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultSepoliaGenesisBlock returns the Sepolia network genesis block.
|
|
||||||
func DefaultSepoliaGenesisBlock() *Genesis {
|
|
||||||
return &Genesis{
|
|
||||||
Config: params.SepoliaChainConfig,
|
|
||||||
Nonce: 0,
|
|
||||||
ExtraData: []byte("Sepolia, Athens, Attica, Greece!"),
|
|
||||||
GasLimit: 0x1c9c380,
|
|
||||||
Difficulty: big.NewInt(0x20000),
|
|
||||||
Timestamp: 1633267481,
|
|
||||||
Alloc: decodePrealloc(sepoliaAllocData),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultHoleskyGenesisBlock returns the Holesky network genesis block.
|
|
||||||
func DefaultHoleskyGenesisBlock() *Genesis {
|
|
||||||
return &Genesis{
|
|
||||||
Config: params.HoleskyChainConfig,
|
|
||||||
Nonce: 0x1234,
|
|
||||||
GasLimit: 0x17d7840,
|
|
||||||
Difficulty: big.NewInt(0x01),
|
|
||||||
Timestamp: 1695902100,
|
|
||||||
Alloc: decodePrealloc(holeskyAllocData),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeveloperGenesisBlock returns the 'geth --dev' genesis block.
|
|
||||||
func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis {
|
|
||||||
// Override the default period to the user requested one
|
|
||||||
config := *params.AllDevChainProtocolChanges
|
|
||||||
|
|
||||||
// Assemble and return the genesis with the precompiles and faucet pre-funded
|
|
||||||
genesis := &Genesis{
|
|
||||||
Config: &config,
|
|
||||||
GasLimit: gasLimit,
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Difficulty: big.NewInt(1),
|
|
||||||
Alloc: map[common.Address]GenesisAccount{
|
|
||||||
common.BytesToAddress([]byte{1}): {Balance: big.NewInt(1)}, // ECRecover
|
|
||||||
common.BytesToAddress([]byte{2}): {Balance: big.NewInt(1)}, // SHA256
|
|
||||||
common.BytesToAddress([]byte{3}): {Balance: big.NewInt(1)}, // RIPEMD
|
|
||||||
common.BytesToAddress([]byte{4}): {Balance: big.NewInt(1)}, // Identity
|
|
||||||
common.BytesToAddress([]byte{5}): {Balance: big.NewInt(1)}, // ModExp
|
|
||||||
common.BytesToAddress([]byte{6}): {Balance: big.NewInt(1)}, // ECAdd
|
|
||||||
common.BytesToAddress([]byte{7}): {Balance: big.NewInt(1)}, // ECScalarMul
|
|
||||||
common.BytesToAddress([]byte{8}): {Balance: big.NewInt(1)}, // ECPairing
|
|
||||||
common.BytesToAddress([]byte{9}): {Balance: big.NewInt(1)}, // BLAKE2b
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if faucet != nil {
|
|
||||||
genesis.Alloc[*faucet] = GenesisAccount{Balance: new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(9))}
|
|
||||||
}
|
|
||||||
return genesis
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodePrealloc(data string) GenesisAlloc {
|
|
||||||
var p []struct {
|
|
||||||
Addr *big.Int
|
|
||||||
Balance *big.Int
|
|
||||||
Misc *struct {
|
|
||||||
Nonce uint64
|
|
||||||
Code []byte
|
|
||||||
Slots []struct {
|
|
||||||
Key common.Hash
|
|
||||||
Val common.Hash
|
|
||||||
}
|
|
||||||
} `rlp:"optional"`
|
|
||||||
}
|
|
||||||
if err := rlp.NewStream(strings.NewReader(data), 0).Decode(&p); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
ga := make(GenesisAlloc, len(p))
|
|
||||||
for _, account := range p {
|
|
||||||
acc := GenesisAccount{Balance: account.Balance}
|
|
||||||
if account.Misc != nil {
|
|
||||||
acc.Nonce = account.Misc.Nonce
|
|
||||||
acc.Code = account.Misc.Code
|
|
||||||
|
|
||||||
acc.Storage = make(map[common.Hash]common.Hash)
|
|
||||||
for _, slot := range account.Misc.Slots {
|
|
||||||
acc.Storage[slot.Key] = slot.Val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ga[common.BigToAddress(account.Addr)] = acc
|
|
||||||
}
|
|
||||||
return ga
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,327 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/vm"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/triedb/pathdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInvalidCliqueConfig(t *testing.T) {
|
|
||||||
block := DefaultGoerliGenesisBlock()
|
|
||||||
block.ExtraData = []byte{}
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
if _, err := block.Commit(db, trie.NewDatabase(db, nil)); err == nil {
|
|
||||||
t.Fatal("Expected error on invalid clique config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetupGenesis(t *testing.T) {
|
|
||||||
testSetupGenesis(t, rawdb.HashScheme)
|
|
||||||
testSetupGenesis(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSetupGenesis(t *testing.T, scheme string) {
|
|
||||||
var (
|
|
||||||
customghash = common.HexToHash("0x89c99d90b79719238d2645c7642f2c9295246e80775b38cfd162b696817fbd50")
|
|
||||||
customg = Genesis{
|
|
||||||
Config: ¶ms.ChainConfig{HomesteadBlock: big.NewInt(3)},
|
|
||||||
Alloc: GenesisAlloc{
|
|
||||||
{1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
oldcustomg = customg
|
|
||||||
)
|
|
||||||
oldcustomg.Config = ¶ms.ChainConfig{HomesteadBlock: big.NewInt(2)}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fn func(ethdb.Database) (*params.ChainConfig, common.Hash, error)
|
|
||||||
wantConfig *params.ChainConfig
|
|
||||||
wantHash common.Hash
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "genesis without ChainConfig",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
return SetupGenesisBlock(db, trie.NewDatabase(db, newDbConfig(scheme)), new(Genesis))
|
|
||||||
},
|
|
||||||
wantErr: errGenesisNoConfig,
|
|
||||||
wantConfig: params.AllEthashProtocolChanges,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no block in DB, genesis == nil",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
return SetupGenesisBlock(db, trie.NewDatabase(db, newDbConfig(scheme)), nil)
|
|
||||||
},
|
|
||||||
wantHash: params.MainnetGenesisHash,
|
|
||||||
wantConfig: params.MainnetChainConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mainnet block in DB, genesis == nil",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
DefaultGenesisBlock().MustCommit(db, trie.NewDatabase(db, newDbConfig(scheme)))
|
|
||||||
return SetupGenesisBlock(db, trie.NewDatabase(db, newDbConfig(scheme)), nil)
|
|
||||||
},
|
|
||||||
wantHash: params.MainnetGenesisHash,
|
|
||||||
wantConfig: params.MainnetChainConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom block in DB, genesis == nil",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
tdb := trie.NewDatabase(db, newDbConfig(scheme))
|
|
||||||
customg.Commit(db, tdb)
|
|
||||||
return SetupGenesisBlock(db, tdb, nil)
|
|
||||||
},
|
|
||||||
wantHash: customghash,
|
|
||||||
wantConfig: customg.Config,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom block in DB, genesis == goerli",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
tdb := trie.NewDatabase(db, newDbConfig(scheme))
|
|
||||||
customg.Commit(db, tdb)
|
|
||||||
return SetupGenesisBlock(db, tdb, DefaultGoerliGenesisBlock())
|
|
||||||
},
|
|
||||||
wantErr: &GenesisMismatchError{Stored: customghash, New: params.GoerliGenesisHash},
|
|
||||||
wantHash: params.GoerliGenesisHash,
|
|
||||||
wantConfig: params.GoerliChainConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "compatible config in DB",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
tdb := trie.NewDatabase(db, newDbConfig(scheme))
|
|
||||||
oldcustomg.Commit(db, tdb)
|
|
||||||
return SetupGenesisBlock(db, tdb, &customg)
|
|
||||||
},
|
|
||||||
wantHash: customghash,
|
|
||||||
wantConfig: customg.Config,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "incompatible config in DB",
|
|
||||||
fn: func(db ethdb.Database) (*params.ChainConfig, common.Hash, error) {
|
|
||||||
// Commit the 'old' genesis block with Homestead transition at #2.
|
|
||||||
// Advance to block #4, past the homestead transition block of customg.
|
|
||||||
tdb := trie.NewDatabase(db, newDbConfig(scheme))
|
|
||||||
oldcustomg.Commit(db, tdb)
|
|
||||||
|
|
||||||
bc, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(scheme), &oldcustomg, nil, ethash.NewFullFaker(), vm.Config{}, nil, nil)
|
|
||||||
defer bc.Stop()
|
|
||||||
|
|
||||||
_, blocks, _ := GenerateChainWithGenesis(&oldcustomg, ethash.NewFaker(), 4, nil)
|
|
||||||
bc.InsertChain(blocks)
|
|
||||||
|
|
||||||
// This should return a compatibility error.
|
|
||||||
return SetupGenesisBlock(db, tdb, &customg)
|
|
||||||
},
|
|
||||||
wantHash: customghash,
|
|
||||||
wantConfig: customg.Config,
|
|
||||||
wantErr: ¶ms.ConfigCompatError{
|
|
||||||
What: "Homestead fork block",
|
|
||||||
StoredBlock: big.NewInt(2),
|
|
||||||
NewBlock: big.NewInt(3),
|
|
||||||
RewindToBlock: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
config, hash, err := test.fn(db)
|
|
||||||
// Check the return values.
|
|
||||||
if !reflect.DeepEqual(err, test.wantErr) {
|
|
||||||
spew := spew.ConfigState{DisablePointerAddresses: true, DisableCapacities: true}
|
|
||||||
t.Errorf("%s: returned error %#v, want %#v", test.name, spew.NewFormatter(err), spew.NewFormatter(test.wantErr))
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(config, test.wantConfig) {
|
|
||||||
t.Errorf("%s:\nreturned %v\nwant %v", test.name, config, test.wantConfig)
|
|
||||||
}
|
|
||||||
if hash != test.wantHash {
|
|
||||||
t.Errorf("%s: returned hash %s, want %s", test.name, hash.Hex(), test.wantHash.Hex())
|
|
||||||
} else if err == nil {
|
|
||||||
// Check database content.
|
|
||||||
stored := rawdb.ReadBlock(db, test.wantHash, 0)
|
|
||||||
if stored.Hash() != test.wantHash {
|
|
||||||
t.Errorf("%s: block in DB has hash %s, want %s", test.name, stored.Hash(), test.wantHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenesisHashes checks the congruity of default genesis data to
|
|
||||||
// corresponding hardcoded genesis hash values.
|
|
||||||
func TestGenesisHashes(t *testing.T) {
|
|
||||||
for i, c := range []struct {
|
|
||||||
genesis *Genesis
|
|
||||||
want common.Hash
|
|
||||||
}{
|
|
||||||
{DefaultGenesisBlock(), params.MainnetGenesisHash},
|
|
||||||
{DefaultGoerliGenesisBlock(), params.GoerliGenesisHash},
|
|
||||||
{DefaultSepoliaGenesisBlock(), params.SepoliaGenesisHash},
|
|
||||||
} {
|
|
||||||
// Test via MustCommit
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
if have := c.genesis.MustCommit(db, trie.NewDatabase(db, trie.HashDefaults)).Hash(); have != c.want {
|
|
||||||
t.Errorf("case: %d a), want: %s, got: %s", i, c.want.Hex(), have.Hex())
|
|
||||||
}
|
|
||||||
// Test via ToBlock
|
|
||||||
if have := c.genesis.ToBlock().Hash(); have != c.want {
|
|
||||||
t.Errorf("case: %d a), want: %s, got: %s", i, c.want.Hex(), have.Hex())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenesis_Commit(t *testing.T) {
|
|
||||||
genesis := &Genesis{
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Config: params.TestChainConfig,
|
|
||||||
// difficulty is nil
|
|
||||||
}
|
|
||||||
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
genesisBlock := genesis.MustCommit(db, trie.NewDatabase(db, trie.HashDefaults))
|
|
||||||
|
|
||||||
if genesis.Difficulty != nil {
|
|
||||||
t.Fatalf("assumption wrong")
|
|
||||||
}
|
|
||||||
|
|
||||||
// This value should have been set as default in the ToBlock method.
|
|
||||||
if genesisBlock.Difficulty().Cmp(params.GenesisDifficulty) != 0 {
|
|
||||||
t.Errorf("assumption wrong: want: %d, got: %v", params.GenesisDifficulty, genesisBlock.Difficulty())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect the stored total difficulty to be the difficulty of the genesis block.
|
|
||||||
stored := rawdb.ReadTd(db, genesisBlock.Hash(), genesisBlock.NumberU64())
|
|
||||||
|
|
||||||
if stored.Cmp(genesisBlock.Difficulty()) != 0 {
|
|
||||||
t.Errorf("inequal difficulty; stored: %v, genesisBlock: %v", stored, genesisBlock.Difficulty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReadWriteGenesisAlloc(t *testing.T) {
|
|
||||||
var (
|
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
alloc = &GenesisAlloc{
|
|
||||||
{1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}},
|
|
||||||
{2}: {Balance: big.NewInt(2), Storage: map[common.Hash]common.Hash{{2}: {2}}},
|
|
||||||
}
|
|
||||||
hash, _ = alloc.hash(false)
|
|
||||||
)
|
|
||||||
blob, _ := json.Marshal(alloc)
|
|
||||||
rawdb.WriteGenesisStateSpec(db, hash, blob)
|
|
||||||
|
|
||||||
var reload GenesisAlloc
|
|
||||||
err := reload.UnmarshalJSON(rawdb.ReadGenesisStateSpec(db, hash))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to load genesis state %v", err)
|
|
||||||
}
|
|
||||||
if len(reload) != len(*alloc) {
|
|
||||||
t.Fatal("Unexpected genesis allocation")
|
|
||||||
}
|
|
||||||
for addr, account := range reload {
|
|
||||||
want, ok := (*alloc)[addr]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Account is not found")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(want, account) {
|
|
||||||
t.Fatal("Unexpected account")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDbConfig(scheme string) *trie.Config {
|
|
||||||
if scheme == rawdb.HashScheme {
|
|
||||||
return trie.HashDefaults
|
|
||||||
}
|
|
||||||
return &trie.Config{PathDB: pathdb.Defaults}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerkleGenesisCommit(t *testing.T) {
|
|
||||||
var verkleTime uint64 = 0
|
|
||||||
verkleConfig := ¶ms.ChainConfig{
|
|
||||||
ChainID: big.NewInt(1),
|
|
||||||
HomesteadBlock: big.NewInt(0),
|
|
||||||
DAOForkBlock: nil,
|
|
||||||
DAOForkSupport: false,
|
|
||||||
EIP150Block: big.NewInt(0),
|
|
||||||
EIP155Block: big.NewInt(0),
|
|
||||||
EIP158Block: big.NewInt(0),
|
|
||||||
ByzantiumBlock: big.NewInt(0),
|
|
||||||
ConstantinopleBlock: big.NewInt(0),
|
|
||||||
PetersburgBlock: big.NewInt(0),
|
|
||||||
IstanbulBlock: big.NewInt(0),
|
|
||||||
MuirGlacierBlock: big.NewInt(0),
|
|
||||||
BerlinBlock: big.NewInt(0),
|
|
||||||
LondonBlock: big.NewInt(0),
|
|
||||||
ArrowGlacierBlock: big.NewInt(0),
|
|
||||||
GrayGlacierBlock: big.NewInt(0),
|
|
||||||
MergeNetsplitBlock: nil,
|
|
||||||
ShanghaiTime: &verkleTime,
|
|
||||||
CancunTime: &verkleTime,
|
|
||||||
PragueTime: &verkleTime,
|
|
||||||
VerkleTime: &verkleTime,
|
|
||||||
TerminalTotalDifficulty: big.NewInt(0),
|
|
||||||
TerminalTotalDifficultyPassed: true,
|
|
||||||
Ethash: nil,
|
|
||||||
Clique: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
genesis := &Genesis{
|
|
||||||
BaseFee: big.NewInt(params.InitialBaseFee),
|
|
||||||
Config: verkleConfig,
|
|
||||||
Timestamp: verkleTime,
|
|
||||||
Difficulty: big.NewInt(0),
|
|
||||||
Alloc: GenesisAlloc{
|
|
||||||
{1}: {Balance: big.NewInt(1), Storage: map[common.Hash]common.Hash{{1}: {1}}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := common.Hex2Bytes("14398d42be3394ff8d50681816a4b7bf8d8283306f577faba2d5bc57498de23b")
|
|
||||||
got := genesis.ToBlock().Root().Bytes()
|
|
||||||
if !bytes.Equal(got, expected) {
|
|
||||||
t.Fatalf("invalid genesis state root, expected %x, got %x", expected, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
triedb := trie.NewDatabase(db, &trie.Config{IsVerkle: true, PathDB: pathdb.Defaults})
|
|
||||||
block := genesis.MustCommit(db, triedb)
|
|
||||||
if !bytes.Equal(block.Root().Bytes(), expected) {
|
|
||||||
t.Fatalf("invalid genesis state root, expected %x, got %x", expected, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that the trie is verkle
|
|
||||||
if !triedb.IsVerkle() {
|
|
||||||
t.Fatalf("expected trie to be verkle")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !rawdb.ExistsAccountTrieNode(db, nil) {
|
|
||||||
t.Fatal("could not find node")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,678 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
crand "crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
mrand "math/rand"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/lru"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"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"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
headerCacheLimit = 512
|
|
||||||
tdCacheLimit = 1024
|
|
||||||
numberCacheLimit = 2048
|
|
||||||
)
|
|
||||||
|
|
||||||
// HeaderChain implements the basic block header chain logic that is shared by
|
|
||||||
// core.BlockChain and light.LightChain. It is not usable in itself, only as
|
|
||||||
// a part of either structure.
|
|
||||||
//
|
|
||||||
// HeaderChain is responsible for maintaining the header chain including the
|
|
||||||
// header query and updating.
|
|
||||||
//
|
|
||||||
// The components maintained by headerchain includes: (1) total difficulty
|
|
||||||
// (2) header (3) block hash -> number mapping (4) canonical number -> hash mapping
|
|
||||||
// and (5) head header flag.
|
|
||||||
//
|
|
||||||
// It is not thread safe either, the encapsulating chain structures should do
|
|
||||||
// the necessary mutex locking/unlocking.
|
|
||||||
type HeaderChain struct {
|
|
||||||
config *params.ChainConfig
|
|
||||||
chainDb ethdb.Database
|
|
||||||
genesisHeader *types.Header
|
|
||||||
|
|
||||||
currentHeader atomic.Value // Current head of the header chain (may be above the block chain!)
|
|
||||||
currentHeaderHash common.Hash // Hash of the current head of the header chain (prevent recomputing all the time)
|
|
||||||
|
|
||||||
headerCache *lru.Cache[common.Hash, *types.Header]
|
|
||||||
tdCache *lru.Cache[common.Hash, *big.Int] // most recent total difficulties
|
|
||||||
numberCache *lru.Cache[common.Hash, uint64] // most recent block numbers
|
|
||||||
|
|
||||||
procInterrupt func() bool
|
|
||||||
|
|
||||||
rand *mrand.Rand
|
|
||||||
engine consensus.Engine
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHeaderChain creates a new HeaderChain structure. ProcInterrupt points
|
|
||||||
// to the parent's interrupt semaphore.
|
|
||||||
func NewHeaderChain(chainDb ethdb.Database, config *params.ChainConfig, engine consensus.Engine, procInterrupt func() bool) (*HeaderChain, error) {
|
|
||||||
// Seed a fast but crypto originating random generator
|
|
||||||
seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hc := &HeaderChain{
|
|
||||||
config: config,
|
|
||||||
chainDb: chainDb,
|
|
||||||
headerCache: lru.NewCache[common.Hash, *types.Header](headerCacheLimit),
|
|
||||||
tdCache: lru.NewCache[common.Hash, *big.Int](tdCacheLimit),
|
|
||||||
numberCache: lru.NewCache[common.Hash, uint64](numberCacheLimit),
|
|
||||||
procInterrupt: procInterrupt,
|
|
||||||
rand: mrand.New(mrand.NewSource(seed.Int64())),
|
|
||||||
engine: engine,
|
|
||||||
}
|
|
||||||
hc.genesisHeader = hc.GetHeaderByNumber(0)
|
|
||||||
if hc.genesisHeader == nil {
|
|
||||||
return nil, ErrNoGenesis
|
|
||||||
}
|
|
||||||
hc.currentHeader.Store(hc.genesisHeader)
|
|
||||||
if head := rawdb.ReadHeadBlockHash(chainDb); head != (common.Hash{}) {
|
|
||||||
if chead := hc.GetHeaderByHash(head); chead != nil {
|
|
||||||
hc.currentHeader.Store(chead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hc.currentHeaderHash = hc.CurrentHeader().Hash()
|
|
||||||
headHeaderGauge.Update(hc.CurrentHeader().Number.Int64())
|
|
||||||
return hc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockNumber retrieves the block number belonging to the given hash
|
|
||||||
// from the cache or database
|
|
||||||
func (hc *HeaderChain) GetBlockNumber(hash common.Hash) *uint64 {
|
|
||||||
if cached, ok := hc.numberCache.Get(hash); ok {
|
|
||||||
return &cached
|
|
||||||
}
|
|
||||||
number := rawdb.ReadHeaderNumber(hc.chainDb, hash)
|
|
||||||
if number != nil {
|
|
||||||
hc.numberCache.Add(hash, *number)
|
|
||||||
}
|
|
||||||
return number
|
|
||||||
}
|
|
||||||
|
|
||||||
type headerWriteResult struct {
|
|
||||||
status WriteStatus
|
|
||||||
ignored int
|
|
||||||
imported int
|
|
||||||
lastHash common.Hash
|
|
||||||
lastHeader *types.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reorg reorgs the local canonical chain into the specified chain. The reorg
|
|
||||||
// can be classified into two cases: (a) extend the local chain (b) switch the
|
|
||||||
// head to the given header.
|
|
||||||
func (hc *HeaderChain) Reorg(headers []*types.Header) error {
|
|
||||||
// Short circuit if nothing to reorg.
|
|
||||||
if len(headers) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If the parent of the (first) block is already the canon header,
|
|
||||||
// we don't have to go backwards to delete canon blocks, but simply
|
|
||||||
// pile them onto the existing chain. Otherwise, do the necessary
|
|
||||||
// reorgs.
|
|
||||||
var (
|
|
||||||
first = headers[0]
|
|
||||||
last = headers[len(headers)-1]
|
|
||||||
batch = hc.chainDb.NewBatch()
|
|
||||||
)
|
|
||||||
if first.ParentHash != hc.currentHeaderHash {
|
|
||||||
// Delete any canonical number assignments above the new head
|
|
||||||
for i := last.Number.Uint64() + 1; ; i++ {
|
|
||||||
hash := rawdb.ReadCanonicalHash(hc.chainDb, i)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
rawdb.DeleteCanonicalHash(batch, i)
|
|
||||||
}
|
|
||||||
// Overwrite any stale canonical number assignments, going
|
|
||||||
// backwards from the first header in this import until the
|
|
||||||
// cross link between two chains.
|
|
||||||
var (
|
|
||||||
header = first
|
|
||||||
headNumber = header.Number.Uint64()
|
|
||||||
headHash = header.Hash()
|
|
||||||
)
|
|
||||||
for rawdb.ReadCanonicalHash(hc.chainDb, headNumber) != headHash {
|
|
||||||
rawdb.WriteCanonicalHash(batch, headHash, headNumber)
|
|
||||||
if headNumber == 0 {
|
|
||||||
break // It shouldn't be reached
|
|
||||||
}
|
|
||||||
headHash, headNumber = header.ParentHash, header.Number.Uint64()-1
|
|
||||||
header = hc.GetHeader(headHash, headNumber)
|
|
||||||
if header == nil {
|
|
||||||
return fmt.Errorf("missing parent %d %x", headNumber, headHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Extend the canonical chain with the new headers
|
|
||||||
for i := 0; i < len(headers)-1; i++ {
|
|
||||||
hash := headers[i+1].ParentHash // Save some extra hashing
|
|
||||||
num := headers[i].Number.Uint64()
|
|
||||||
rawdb.WriteCanonicalHash(batch, hash, num)
|
|
||||||
rawdb.WriteHeadHeaderHash(batch, hash)
|
|
||||||
}
|
|
||||||
// Write the last header
|
|
||||||
hash := headers[len(headers)-1].Hash()
|
|
||||||
num := headers[len(headers)-1].Number.Uint64()
|
|
||||||
rawdb.WriteCanonicalHash(batch, hash, num)
|
|
||||||
rawdb.WriteHeadHeaderHash(batch, hash)
|
|
||||||
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Last step update all in-memory head header markers
|
|
||||||
hc.currentHeaderHash = last.Hash()
|
|
||||||
hc.currentHeader.Store(types.CopyHeader(last))
|
|
||||||
headHeaderGauge.Update(last.Number.Int64())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeaders writes a chain of headers into the local chain, given that the
|
|
||||||
// parents are already known. The chain head header won't be updated in this
|
|
||||||
// function, the additional SetCanonical is expected in order to finish the entire
|
|
||||||
// procedure.
|
|
||||||
func (hc *HeaderChain) WriteHeaders(headers []*types.Header) (int, error) {
|
|
||||||
if len(headers) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
ptd := hc.GetTd(headers[0].ParentHash, headers[0].Number.Uint64()-1)
|
|
||||||
if ptd == nil {
|
|
||||||
return 0, consensus.ErrUnknownAncestor
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
newTD = new(big.Int).Set(ptd) // Total difficulty of inserted chain
|
|
||||||
inserted []rawdb.NumberHash // Ephemeral lookup of number/hash for the chain
|
|
||||||
parentKnown = true // Set to true to force hc.HasHeader check the first iteration
|
|
||||||
batch = hc.chainDb.NewBatch()
|
|
||||||
)
|
|
||||||
for i, header := range headers {
|
|
||||||
var hash common.Hash
|
|
||||||
// The headers have already been validated at this point, so we already
|
|
||||||
// know that it's a contiguous chain, where
|
|
||||||
// headers[i].Hash() == headers[i+1].ParentHash
|
|
||||||
if i < len(headers)-1 {
|
|
||||||
hash = headers[i+1].ParentHash
|
|
||||||
} else {
|
|
||||||
hash = header.Hash()
|
|
||||||
}
|
|
||||||
number := header.Number.Uint64()
|
|
||||||
newTD.Add(newTD, header.Difficulty)
|
|
||||||
|
|
||||||
// If the parent was not present, store it
|
|
||||||
// If the header is already known, skip it, otherwise store
|
|
||||||
alreadyKnown := parentKnown && hc.HasHeader(hash, number)
|
|
||||||
if !alreadyKnown {
|
|
||||||
// Irrelevant of the canonical status, write the TD and header to the database.
|
|
||||||
rawdb.WriteTd(batch, hash, number, newTD)
|
|
||||||
hc.tdCache.Add(hash, new(big.Int).Set(newTD))
|
|
||||||
|
|
||||||
rawdb.WriteHeader(batch, header)
|
|
||||||
inserted = append(inserted, rawdb.NumberHash{Number: number, Hash: hash})
|
|
||||||
hc.headerCache.Add(hash, header)
|
|
||||||
hc.numberCache.Add(hash, number)
|
|
||||||
}
|
|
||||||
parentKnown = alreadyKnown
|
|
||||||
}
|
|
||||||
// Skip the slow disk write of all headers if interrupted.
|
|
||||||
if hc.procInterrupt() {
|
|
||||||
log.Debug("Premature abort during headers import")
|
|
||||||
return 0, errors.New("aborted")
|
|
||||||
}
|
|
||||||
// Commit to disk!
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to write headers", "error", err)
|
|
||||||
}
|
|
||||||
return len(inserted), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeHeadersAndSetHead writes a batch of block headers and applies the last
|
|
||||||
// header as the chain head if the fork choicer says it's ok to update the chain.
|
|
||||||
// Note: This method is not concurrent-safe with inserting blocks simultaneously
|
|
||||||
// into the chain, as side effects caused by reorganisations cannot be emulated
|
|
||||||
// without the real blocks. Hence, writing headers directly should only be done
|
|
||||||
// in two scenarios: pure-header mode of operation (light clients), or properly
|
|
||||||
// separated header/block phases (non-archive clients).
|
|
||||||
func (hc *HeaderChain) writeHeadersAndSetHead(headers []*types.Header, forker *ForkChoice) (*headerWriteResult, error) {
|
|
||||||
inserted, err := hc.WriteHeaders(headers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
lastHeader = headers[len(headers)-1]
|
|
||||||
lastHash = headers[len(headers)-1].Hash()
|
|
||||||
result = &headerWriteResult{
|
|
||||||
status: NonStatTy,
|
|
||||||
ignored: len(headers) - inserted,
|
|
||||||
imported: inserted,
|
|
||||||
lastHash: lastHash,
|
|
||||||
lastHeader: lastHeader,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Ask the fork choicer if the reorg is necessary
|
|
||||||
if reorg, err := forker.ReorgNeeded(hc.CurrentHeader(), lastHeader); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !reorg {
|
|
||||||
if inserted != 0 {
|
|
||||||
result.status = SideStatTy
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
// Special case, all the inserted headers are already on the canonical
|
|
||||||
// header chain, skip the reorg operation.
|
|
||||||
if hc.GetCanonicalHash(lastHeader.Number.Uint64()) == lastHash && lastHeader.Number.Uint64() <= hc.CurrentHeader().Number.Uint64() {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
// Apply the reorg operation
|
|
||||||
if err := hc.Reorg(headers); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.status = CanonStatTy
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HeaderChain) ValidateHeaderChain(chain []*types.Header) (int, error) {
|
|
||||||
// Do a sanity check that the provided chain is actually ordered and linked
|
|
||||||
for i := 1; i < len(chain); i++ {
|
|
||||||
if chain[i].Number.Uint64() != chain[i-1].Number.Uint64()+1 {
|
|
||||||
hash := chain[i].Hash()
|
|
||||||
parentHash := chain[i-1].Hash()
|
|
||||||
// Chain broke ancestry, log a message (programming error) and skip insertion
|
|
||||||
log.Error("Non contiguous header insert", "number", chain[i].Number, "hash", hash,
|
|
||||||
"parent", chain[i].ParentHash, "prevnumber", chain[i-1].Number, "prevhash", parentHash)
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x..], item %d is #%d [%x..] (parent [%x..])", i-1, chain[i-1].Number,
|
|
||||||
parentHash.Bytes()[:4], i, chain[i].Number, hash.Bytes()[:4], chain[i].ParentHash[:4])
|
|
||||||
}
|
|
||||||
// If the header is a banned one, straight out abort
|
|
||||||
if BadHashes[chain[i].ParentHash] {
|
|
||||||
return i - 1, ErrBannedHash
|
|
||||||
}
|
|
||||||
// If it's the last header in the cunk, we need to check it too
|
|
||||||
if i == len(chain)-1 && BadHashes[chain[i].Hash()] {
|
|
||||||
return i, ErrBannedHash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Start the parallel verifier
|
|
||||||
abort, results := hc.engine.VerifyHeaders(hc, chain)
|
|
||||||
defer close(abort)
|
|
||||||
|
|
||||||
// Iterate over the headers and ensure they all check out
|
|
||||||
for i := range chain {
|
|
||||||
// If the chain is terminating, stop processing blocks
|
|
||||||
if hc.procInterrupt() {
|
|
||||||
log.Debug("Premature abort during headers verification")
|
|
||||||
return 0, errors.New("aborted")
|
|
||||||
}
|
|
||||||
// Otherwise wait for headers checks and ensure they pass
|
|
||||||
if err := <-results; err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertHeaderChain inserts the given headers and does the reorganisations.
|
|
||||||
//
|
|
||||||
// The validity of the headers is NOT CHECKED by this method, i.e. they need to be
|
|
||||||
// validated by ValidateHeaderChain before calling InsertHeaderChain.
|
|
||||||
//
|
|
||||||
// This insert is all-or-nothing. If this returns an error, no headers were written,
|
|
||||||
// otherwise they were all processed successfully.
|
|
||||||
//
|
|
||||||
// The returned 'write status' says if the inserted headers are part of the canonical chain
|
|
||||||
// or a side chain.
|
|
||||||
func (hc *HeaderChain) InsertHeaderChain(chain []*types.Header, start time.Time, forker *ForkChoice) (WriteStatus, error) {
|
|
||||||
if hc.procInterrupt() {
|
|
||||||
return 0, errors.New("aborted")
|
|
||||||
}
|
|
||||||
res, err := hc.writeHeadersAndSetHead(chain, forker)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Report some public statistics so the user has a clue what's going on
|
|
||||||
context := []interface{}{
|
|
||||||
"count", res.imported,
|
|
||||||
"elapsed", common.PrettyDuration(time.Since(start)),
|
|
||||||
}
|
|
||||||
if last := res.lastHeader; last != nil {
|
|
||||||
context = append(context, "number", last.Number, "hash", res.lastHash)
|
|
||||||
if timestamp := time.Unix(int64(last.Time), 0); time.Since(timestamp) > time.Minute {
|
|
||||||
context = append(context, []interface{}{"age", common.PrettyAge(timestamp)}...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if res.ignored > 0 {
|
|
||||||
context = append(context, []interface{}{"ignored", res.ignored}...)
|
|
||||||
}
|
|
||||||
log.Debug("Imported new block headers", context...)
|
|
||||||
return res.status, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAncestor retrieves the Nth ancestor of a given block. It assumes that either the given block or
|
|
||||||
// a close ancestor of it is canonical. maxNonCanonical points to a downwards counter limiting the
|
|
||||||
// number of blocks to be individually checked before we reach the canonical chain.
|
|
||||||
//
|
|
||||||
// Note: ancestor == 0 returns the same block, 1 returns its parent and so on.
|
|
||||||
func (hc *HeaderChain) GetAncestor(hash common.Hash, number, ancestor uint64, maxNonCanonical *uint64) (common.Hash, uint64) {
|
|
||||||
if ancestor > number {
|
|
||||||
return common.Hash{}, 0
|
|
||||||
}
|
|
||||||
if ancestor == 1 {
|
|
||||||
// in this case it is cheaper to just read the header
|
|
||||||
if header := hc.GetHeader(hash, number); header != nil {
|
|
||||||
return header.ParentHash, number - 1
|
|
||||||
}
|
|
||||||
return common.Hash{}, 0
|
|
||||||
}
|
|
||||||
for ancestor != 0 {
|
|
||||||
if rawdb.ReadCanonicalHash(hc.chainDb, number) == hash {
|
|
||||||
ancestorHash := rawdb.ReadCanonicalHash(hc.chainDb, number-ancestor)
|
|
||||||
if rawdb.ReadCanonicalHash(hc.chainDb, number) == hash {
|
|
||||||
number -= ancestor
|
|
||||||
return ancestorHash, number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if *maxNonCanonical == 0 {
|
|
||||||
return common.Hash{}, 0
|
|
||||||
}
|
|
||||||
*maxNonCanonical--
|
|
||||||
ancestor--
|
|
||||||
header := hc.GetHeader(hash, number)
|
|
||||||
if header == nil {
|
|
||||||
return common.Hash{}, 0
|
|
||||||
}
|
|
||||||
hash = header.ParentHash
|
|
||||||
number--
|
|
||||||
}
|
|
||||||
return hash, number
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTd retrieves a block's total difficulty in the canonical chain from the
|
|
||||||
// database by hash and number, caching it if found.
|
|
||||||
func (hc *HeaderChain) GetTd(hash common.Hash, number uint64) *big.Int {
|
|
||||||
// Short circuit if the td's already in the cache, retrieve otherwise
|
|
||||||
if cached, ok := hc.tdCache.Get(hash); ok {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
td := rawdb.ReadTd(hc.chainDb, hash, number)
|
|
||||||
if td == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Cache the found body for next time and return
|
|
||||||
hc.tdCache.Add(hash, td)
|
|
||||||
return td
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeader retrieves a block header from the database by hash and number,
|
|
||||||
// caching it if found.
|
|
||||||
func (hc *HeaderChain) GetHeader(hash common.Hash, number uint64) *types.Header {
|
|
||||||
// Short circuit if the header's already in the cache, retrieve otherwise
|
|
||||||
if header, ok := hc.headerCache.Get(hash); ok {
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
header := rawdb.ReadHeader(hc.chainDb, hash, number)
|
|
||||||
if header == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Cache the found header for next time and return
|
|
||||||
hc.headerCache.Add(hash, header)
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeaderByHash retrieves a block header from the database by hash, caching it if
|
|
||||||
// found.
|
|
||||||
func (hc *HeaderChain) GetHeaderByHash(hash common.Hash) *types.Header {
|
|
||||||
number := hc.GetBlockNumber(hash)
|
|
||||||
if number == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return hc.GetHeader(hash, *number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasHeader checks if a block header is present in the database or not.
|
|
||||||
// In theory, if header is present in the database, all relative components
|
|
||||||
// like td and hash->number should be present too.
|
|
||||||
func (hc *HeaderChain) HasHeader(hash common.Hash, number uint64) bool {
|
|
||||||
if hc.numberCache.Contains(hash) || hc.headerCache.Contains(hash) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return rawdb.HasHeader(hc.chainDb, hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeaderByNumber retrieves a block header from the database by number,
|
|
||||||
// caching it (associated with its hash) if found.
|
|
||||||
func (hc *HeaderChain) GetHeaderByNumber(number uint64) *types.Header {
|
|
||||||
hash := rawdb.ReadCanonicalHash(hc.chainDb, number)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return hc.GetHeader(hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHeadersFrom returns a contiguous segment of headers, in rlp-form, going
|
|
||||||
// backwards from the given number.
|
|
||||||
// If the 'number' is higher than the highest local header, this method will
|
|
||||||
// return a best-effort response, containing the headers that we do have.
|
|
||||||
func (hc *HeaderChain) GetHeadersFrom(number, count uint64) []rlp.RawValue {
|
|
||||||
// If the request is for future headers, we still return the portion of
|
|
||||||
// headers that we are able to serve
|
|
||||||
if current := hc.CurrentHeader().Number.Uint64(); current < number {
|
|
||||||
if count > number-current {
|
|
||||||
count -= number - current
|
|
||||||
number = current
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var headers []rlp.RawValue
|
|
||||||
// If we have some of the headers in cache already, use that before going to db.
|
|
||||||
hash := rawdb.ReadCanonicalHash(hc.chainDb, number)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for count > 0 {
|
|
||||||
header, ok := hc.headerCache.Get(hash)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
rlpData, _ := rlp.EncodeToBytes(header)
|
|
||||||
headers = append(headers, rlpData)
|
|
||||||
hash = header.ParentHash
|
|
||||||
count--
|
|
||||||
number--
|
|
||||||
}
|
|
||||||
// Read remaining from db
|
|
||||||
if count > 0 {
|
|
||||||
headers = append(headers, rawdb.ReadHeaderRange(hc.chainDb, number, count)...)
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HeaderChain) GetCanonicalHash(number uint64) common.Hash {
|
|
||||||
return rawdb.ReadCanonicalHash(hc.chainDb, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentHeader retrieves the current head header of the canonical chain. The
|
|
||||||
// header is retrieved from the HeaderChain's internal cache.
|
|
||||||
func (hc *HeaderChain) CurrentHeader() *types.Header {
|
|
||||||
return hc.currentHeader.Load().(*types.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCurrentHeader sets the in-memory head header marker of the canonical chan
|
|
||||||
// as the given header.
|
|
||||||
func (hc *HeaderChain) SetCurrentHeader(head *types.Header) {
|
|
||||||
hc.currentHeader.Store(head)
|
|
||||||
hc.currentHeaderHash = head.Hash()
|
|
||||||
headHeaderGauge.Update(head.Number.Int64())
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
// UpdateHeadBlocksCallback is a callback function that is called by SetHead
|
|
||||||
// before head header is updated. The method will return the actual block it
|
|
||||||
// updated the head to (missing state) and a flag if setHead should continue
|
|
||||||
// rewinding till that forcefully (exceeded ancient limits)
|
|
||||||
UpdateHeadBlocksCallback func(ethdb.KeyValueWriter, *types.Header) (*types.Header, bool)
|
|
||||||
|
|
||||||
// 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, updateFn UpdateHeadBlocksCallback, delFn DeleteBlockContentCallback) {
|
|
||||||
hc.setHead(head, 0, updateFn, delFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHeadWithTimestamp rewinds the local chain to a new head timestamp. Everything
|
|
||||||
// above the new head will be deleted and the new one set.
|
|
||||||
func (hc *HeaderChain) SetHeadWithTimestamp(time uint64, updateFn UpdateHeadBlocksCallback, delFn DeleteBlockContentCallback) {
|
|
||||||
hc.setHead(0, time, updateFn, delFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setHead rewinds the local chain to a new head block or a head timestamp.
|
|
||||||
// Everything above the new head will be deleted and the new one set.
|
|
||||||
func (hc *HeaderChain) setHead(headBlock uint64, headTime uint64, updateFn UpdateHeadBlocksCallback, delFn DeleteBlockContentCallback) {
|
|
||||||
// Sanity check that there's no attempt to undo the genesis block. This is
|
|
||||||
// a fairly synthetic case where someone enables a timestamp based fork
|
|
||||||
// below the genesis timestamp. It's nice to not allow that instead of the
|
|
||||||
// entire chain getting deleted.
|
|
||||||
if headTime > 0 && hc.genesisHeader.Time > headTime {
|
|
||||||
// Note, a critical error is quite brutal, but we should really not reach
|
|
||||||
// this point. Since pre-timestamp based forks it was impossible to have
|
|
||||||
// a fork before block 0, the setHead would always work. With timestamp
|
|
||||||
// forks it becomes possible to specify below the genesis. That said, the
|
|
||||||
// only time we setHead via timestamp is with chain config changes on the
|
|
||||||
// startup, so failing hard there is ok.
|
|
||||||
log.Crit("Rejecting genesis rewind via timestamp", "target", headTime, "genesis", hc.genesisHeader.Time)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
parentHash common.Hash
|
|
||||||
batch = hc.chainDb.NewBatch()
|
|
||||||
origin = true
|
|
||||||
)
|
|
||||||
done := func(header *types.Header) bool {
|
|
||||||
if headTime > 0 {
|
|
||||||
return header.Time <= headTime
|
|
||||||
}
|
|
||||||
return header.Number.Uint64() <= headBlock
|
|
||||||
}
|
|
||||||
for hdr := hc.CurrentHeader(); hdr != nil && !done(hdr); hdr = hc.CurrentHeader() {
|
|
||||||
num := hdr.Number.Uint64()
|
|
||||||
|
|
||||||
// Rewind chain to new head
|
|
||||||
parent := hc.GetHeader(hdr.ParentHash, num-1)
|
|
||||||
if parent == nil {
|
|
||||||
parent = hc.genesisHeader
|
|
||||||
}
|
|
||||||
parentHash = parent.Hash()
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
markerBatch := hc.chainDb.NewBatch()
|
|
||||||
if updateFn != nil {
|
|
||||||
newHead, force := updateFn(markerBatch, parent)
|
|
||||||
if force && ((headTime > 0 && newHead.Time < headTime) || (headTime == 0 && newHead.Number.Uint64() < headBlock)) {
|
|
||||||
log.Warn("Force rewinding till ancient limit", "head", newHead.Number.Uint64())
|
|
||||||
headBlock, headTime = newHead.Number.Uint64(), 0 // Target timestamp passed, continue rewind in block mode (cleaner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update head header then.
|
|
||||||
rawdb.WriteHeadHeaderHash(markerBatch, parentHash)
|
|
||||||
if err := markerBatch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to update chain markers", "error", err)
|
|
||||||
}
|
|
||||||
hc.currentHeader.Store(parent)
|
|
||||||
hc.currentHeaderHash = parentHash
|
|
||||||
headHeaderGauge.Update(parent.Number.Int64())
|
|
||||||
|
|
||||||
// If this is the first iteration, wipe any leftover data upwards too so
|
|
||||||
// we don't end up with dangling daps in the database
|
|
||||||
var nums []uint64
|
|
||||||
if origin {
|
|
||||||
for n := num + 1; len(rawdb.ReadAllHashes(hc.chainDb, n)) > 0; n++ {
|
|
||||||
nums = append([]uint64{n}, nums...) // suboptimal, but we don't really expect this path
|
|
||||||
}
|
|
||||||
origin = false
|
|
||||||
}
|
|
||||||
nums = append(nums, num)
|
|
||||||
|
|
||||||
// Remove the related data from the database on all sidechains
|
|
||||||
for _, num := range nums {
|
|
||||||
// Gather all the side fork hashes
|
|
||||||
hashes := rawdb.ReadAllHashes(hc.chainDb, num)
|
|
||||||
if len(hashes) == 0 {
|
|
||||||
// No hashes in the database whatsoever, probably frozen already
|
|
||||||
hashes = append(hashes, hdr.Hash())
|
|
||||||
}
|
|
||||||
for _, hash := range hashes {
|
|
||||||
if delFn != nil {
|
|
||||||
delFn(batch, hash, num)
|
|
||||||
}
|
|
||||||
rawdb.DeleteHeader(batch, hash, num)
|
|
||||||
rawdb.DeleteTd(batch, hash, num)
|
|
||||||
}
|
|
||||||
rawdb.DeleteCanonicalHash(batch, num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flush all accumulated deletions.
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to rewind block", "error", err)
|
|
||||||
}
|
|
||||||
// Clear out any stale content from the caches
|
|
||||||
hc.headerCache.Purge()
|
|
||||||
hc.tdCache.Purge()
|
|
||||||
hc.numberCache.Purge()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGenesis sets a new genesis block header for the chain
|
|
||||||
func (hc *HeaderChain) SetGenesis(head *types.Header) {
|
|
||||||
hc.genesisHeader = head
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config retrieves the header chain's chain configuration.
|
|
||||||
func (hc *HeaderChain) Config() *params.ChainConfig { return hc.config }
|
|
||||||
|
|
||||||
// Engine retrieves the header chain's consensus engine.
|
|
||||||
func (hc *HeaderChain) Engine() consensus.Engine { return hc.engine }
|
|
||||||
|
|
||||||
// GetBlock implements consensus.ChainReader, and returns nil for every input as
|
|
||||||
// a header chain does not have blocks available for retrieval.
|
|
||||||
func (hc *HeaderChain) GetBlock(hash common.Hash, number uint64) *types.Block {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/consensus"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
func verifyUnbrokenCanonchain(hc *HeaderChain) error {
|
|
||||||
h := hc.CurrentHeader()
|
|
||||||
for {
|
|
||||||
canonHash := rawdb.ReadCanonicalHash(hc.chainDb, h.Number.Uint64())
|
|
||||||
if exp := h.Hash(); canonHash != exp {
|
|
||||||
return fmt.Errorf("Canon hash chain broken, block %d got %x, expected %x",
|
|
||||||
h.Number, canonHash[:8], exp[:8])
|
|
||||||
}
|
|
||||||
// Verify that we have the TD
|
|
||||||
if td := rawdb.ReadTd(hc.chainDb, canonHash, h.Number.Uint64()); td == nil {
|
|
||||||
return fmt.Errorf("Canon TD missing at block %d", h.Number)
|
|
||||||
}
|
|
||||||
if h.Number.Uint64() == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h = hc.GetHeader(h.ParentHash, h.Number.Uint64()-1)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func testInsert(t *testing.T, hc *HeaderChain, chain []*types.Header, wantStatus WriteStatus, wantErr error, forker *ForkChoice) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
status, err := hc.InsertHeaderChain(chain, time.Now(), forker)
|
|
||||||
if status != wantStatus {
|
|
||||||
t.Errorf("wrong write status from InsertHeaderChain: got %v, want %v", status, wantStatus)
|
|
||||||
}
|
|
||||||
// Always verify that the header chain is unbroken
|
|
||||||
if err := verifyUnbrokenCanonchain(hc); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !errors.Is(err, wantErr) {
|
|
||||||
t.Fatalf("unexpected error from InsertHeaderChain: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks status reporting of InsertHeaderChain.
|
|
||||||
func TestHeaderInsertion(t *testing.T) {
|
|
||||||
var (
|
|
||||||
db = rawdb.NewMemoryDatabase()
|
|
||||||
gspec = &Genesis{BaseFee: big.NewInt(params.InitialBaseFee), Config: params.AllEthashProtocolChanges}
|
|
||||||
)
|
|
||||||
gspec.Commit(db, trie.NewDatabase(db, nil))
|
|
||||||
hc, err := NewHeaderChain(db, gspec.Config, ethash.NewFaker(), func() bool { return false })
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// chain A: G->A1->A2...A128
|
|
||||||
genDb, chainA := makeHeaderChainWithGenesis(gspec, 128, ethash.NewFaker(), 10)
|
|
||||||
// chain B: G->A1->B1...B128
|
|
||||||
chainB := makeHeaderChain(gspec.Config, chainA[0], 128, ethash.NewFaker(), genDb, 10)
|
|
||||||
|
|
||||||
forker := NewForkChoice(hc, nil)
|
|
||||||
// Inserting 64 headers on an empty chain, expecting
|
|
||||||
// 1 callbacks, 1 canon-status, 0 sidestatus,
|
|
||||||
testInsert(t, hc, chainA[:64], CanonStatTy, nil, forker)
|
|
||||||
|
|
||||||
// Inserting 64 identical headers, expecting
|
|
||||||
// 0 callbacks, 0 canon-status, 0 sidestatus,
|
|
||||||
testInsert(t, hc, chainA[:64], NonStatTy, nil, forker)
|
|
||||||
|
|
||||||
// Inserting the same some old, some new headers
|
|
||||||
// 1 callbacks, 1 canon, 0 side
|
|
||||||
testInsert(t, hc, chainA[32:96], CanonStatTy, nil, forker)
|
|
||||||
|
|
||||||
// Inserting side blocks, but not overtaking the canon chain
|
|
||||||
testInsert(t, hc, chainB[0:32], SideStatTy, nil, forker)
|
|
||||||
|
|
||||||
// Inserting more side blocks, but we don't have the parent
|
|
||||||
testInsert(t, hc, chainB[34:36], NonStatTy, consensus.ErrUnknownAncestor, forker)
|
|
||||||
|
|
||||||
// Inserting more sideblocks, overtaking the canon chain
|
|
||||||
testInsert(t, hc, chainB[32:97], CanonStatTy, nil, forker)
|
|
||||||
|
|
||||||
// Inserting more A-headers, taking back the canonicality
|
|
||||||
testInsert(t, hc, chainA[90:100], CanonStatTy, nil, forker)
|
|
||||||
|
|
||||||
// And B becomes canon again
|
|
||||||
testInsert(t, hc, chainB[97:107], CanonStatTy, nil, forker)
|
|
||||||
|
|
||||||
// And B becomes even longer
|
|
||||||
testInsert(t, hc, chainB[107:128], CanonStatTy, nil, forker)
|
|
||||||
}
|
|
||||||
108
core/mkalloc.go
108
core/mkalloc.go
|
|
@ -1,108 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
//go:build none
|
|
||||||
// +build none
|
|
||||||
|
|
||||||
/*
|
|
||||||
The mkalloc tool creates the genesis allocation constants in genesis_alloc.go
|
|
||||||
It outputs a const declaration that contains an RLP-encoded list of (address, balance) tuples.
|
|
||||||
|
|
||||||
go run mkalloc.go genesis.json
|
|
||||||
*/
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
type allocItem struct {
|
|
||||||
Addr *big.Int
|
|
||||||
Balance *big.Int
|
|
||||||
Misc *allocItemMisc `rlp:"optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type allocItemMisc struct {
|
|
||||||
Nonce uint64
|
|
||||||
Code []byte
|
|
||||||
Slots []allocItemStorageItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type allocItemStorageItem struct {
|
|
||||||
Key common.Hash
|
|
||||||
Val common.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func makelist(g *core.Genesis) []allocItem {
|
|
||||||
items := make([]allocItem, 0, len(g.Alloc))
|
|
||||||
for addr, account := range g.Alloc {
|
|
||||||
var misc *allocItemMisc
|
|
||||||
if len(account.Storage) > 0 || len(account.Code) > 0 || account.Nonce != 0 {
|
|
||||||
misc = &allocItemMisc{
|
|
||||||
Nonce: account.Nonce,
|
|
||||||
Code: account.Code,
|
|
||||||
Slots: make([]allocItemStorageItem, 0, len(account.Storage)),
|
|
||||||
}
|
|
||||||
for key, val := range account.Storage {
|
|
||||||
misc.Slots = append(misc.Slots, allocItemStorageItem{key, val})
|
|
||||||
}
|
|
||||||
slices.SortFunc(misc.Slots, func(a, b allocItemStorageItem) int {
|
|
||||||
return a.Key.Cmp(b.Key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
bigAddr := new(big.Int).SetBytes(addr.Bytes())
|
|
||||||
items = append(items, allocItem{bigAddr, account.Balance, misc})
|
|
||||||
}
|
|
||||||
slices.SortFunc(items, func(a, b allocItem) int {
|
|
||||||
return a.Addr.Cmp(b.Addr)
|
|
||||||
})
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func makealloc(g *core.Genesis) string {
|
|
||||||
a := makelist(g)
|
|
||||||
data, err := rlp.EncodeToBytes(a)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return strconv.QuoteToASCII(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "Usage: mkalloc genesis.json")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
g := new(core.Genesis)
|
|
||||||
file, err := os.Open(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(file).Decode(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println("const allocData =", makealloc(g))
|
|
||||||
}
|
|
||||||
|
|
@ -1,984 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadCanonicalHash retrieves the hash assigned to a canonical block number.
|
|
||||||
func ReadCanonicalHash(db ethdb.Reader, number uint64) common.Hash {
|
|
||||||
var data []byte
|
|
||||||
db.ReadAncients(func(reader ethdb.AncientReaderOp) error {
|
|
||||||
data, _ = reader.Ancient(ChainFreezerHashTable, number)
|
|
||||||
if len(data) == 0 {
|
|
||||||
// Get it by hash from leveldb
|
|
||||||
data, _ = db.Get(headerHashKey(number))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return common.BytesToHash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteCanonicalHash stores the hash assigned to a canonical block number.
|
|
||||||
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.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,
|
|
||||||
// both canonical and reorged forks included.
|
|
||||||
func ReadAllHashes(db ethdb.Iteratee, number uint64) []common.Hash {
|
|
||||||
prefix := headerKeyPrefix(number)
|
|
||||||
|
|
||||||
hashes := make([]common.Hash, 0, 1)
|
|
||||||
it := db.NewIterator(prefix, nil)
|
|
||||||
defer it.Release()
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
if key := it.Key(); len(key) == len(prefix)+32 {
|
|
||||||
hashes = append(hashes, common.BytesToHash(key[len(key)-32:]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hashes
|
|
||||||
}
|
|
||||||
|
|
||||||
type NumberHash struct {
|
|
||||||
Number uint64
|
|
||||||
Hash common.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAllHashesInRange retrieves all the hashes assigned to blocks at certain
|
|
||||||
// heights, both canonical and reorged forks included.
|
|
||||||
// This method considers both limits to be _inclusive_.
|
|
||||||
func ReadAllHashesInRange(db ethdb.Iteratee, first, last uint64) []*NumberHash {
|
|
||||||
var (
|
|
||||||
start = encodeBlockNumber(first)
|
|
||||||
keyLength = len(headerPrefix) + 8 + 32
|
|
||||||
hashes = make([]*NumberHash, 0, 1+last-first)
|
|
||||||
it = db.NewIterator(headerPrefix, start)
|
|
||||||
)
|
|
||||||
defer it.Release()
|
|
||||||
for it.Next() {
|
|
||||||
key := it.Key()
|
|
||||||
if len(key) != keyLength {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
num := binary.BigEndian.Uint64(key[len(headerPrefix) : len(headerPrefix)+8])
|
|
||||||
if num > last {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
hash := common.BytesToHash(key[len(key)-32:])
|
|
||||||
hashes = append(hashes, &NumberHash{num, hash})
|
|
||||||
}
|
|
||||||
return hashes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAllCanonicalHashes retrieves all canonical number and hash mappings at the
|
|
||||||
// certain chain range. If the accumulated entries reaches the given threshold,
|
|
||||||
// abort the iteration and return the semi-finish result.
|
|
||||||
func ReadAllCanonicalHashes(db ethdb.Iteratee, from uint64, to uint64, limit int) ([]uint64, []common.Hash) {
|
|
||||||
// Short circuit if the limit is 0.
|
|
||||||
if limit == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
numbers []uint64
|
|
||||||
hashes []common.Hash
|
|
||||||
)
|
|
||||||
// Construct the key prefix of start point.
|
|
||||||
start, end := headerHashKey(from), headerHashKey(to)
|
|
||||||
it := db.NewIterator(nil, start)
|
|
||||||
defer it.Release()
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
if bytes.Compare(it.Key(), end) >= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if key := it.Key(); len(key) == len(headerPrefix)+8+1 && bytes.Equal(key[len(key)-1:], headerHashSuffix) {
|
|
||||||
numbers = append(numbers, binary.BigEndian.Uint64(key[len(headerPrefix):len(headerPrefix)+8]))
|
|
||||||
hashes = append(hashes, common.BytesToHash(it.Value()))
|
|
||||||
// If the accumulated entries reaches the limit threshold, return.
|
|
||||||
if len(numbers) >= limit {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return numbers, hashes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeaderNumber returns the header number assigned to a hash.
|
|
||||||
func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 {
|
|
||||||
data, _ := db.Get(headerNumberKey(hash))
|
|
||||||
if len(data) != 8 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
number := binary.BigEndian.Uint64(data)
|
|
||||||
return &number
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeadHeaderHash retrieves the hash of the current canonical head header.
|
|
||||||
func ReadHeadHeaderHash(db ethdb.KeyValueReader) common.Hash {
|
|
||||||
data, _ := db.Get(headHeaderKey)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
return common.BytesToHash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeadHeaderHash stores the hash of the current canonical head header.
|
|
||||||
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.KeyValueReader) common.Hash {
|
|
||||||
data, _ := db.Get(headBlockKey)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
return common.BytesToHash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeadBlockHash stores the head block's 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.KeyValueReader) common.Hash {
|
|
||||||
data, _ := db.Get(headFastBlockKey)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
return common.BytesToHash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeadFastBlockHash stores the hash of the current fast-sync head block.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFinalizedBlockHash retrieves the hash of the finalized block.
|
|
||||||
func ReadFinalizedBlockHash(db ethdb.KeyValueReader) common.Hash {
|
|
||||||
data, _ := db.Get(headFinalizedBlockKey)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
return common.BytesToHash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFinalizedBlockHash stores the hash of the finalized block.
|
|
||||||
func WriteFinalizedBlockHash(db ethdb.KeyValueWriter, hash common.Hash) {
|
|
||||||
if err := db.Put(headFinalizedBlockKey, hash.Bytes()); err != nil {
|
|
||||||
log.Crit("Failed to store last finalized block's hash", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadLastPivotNumber retrieves the number of the last pivot block. If the node
|
|
||||||
// full synced, the last pivot will always be nil.
|
|
||||||
func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 {
|
|
||||||
data, _ := db.Get(lastPivotKey)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var pivot uint64
|
|
||||||
if err := rlp.DecodeBytes(data, &pivot); err != nil {
|
|
||||||
log.Error("Invalid pivot block number in database", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &pivot
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteLastPivotNumber stores the number of the last pivot block.
|
|
||||||
func WriteLastPivotNumber(db ethdb.KeyValueWriter, pivot uint64) {
|
|
||||||
enc, err := rlp.EncodeToBytes(pivot)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to encode pivot block number", "err", err)
|
|
||||||
}
|
|
||||||
if err := db.Put(lastPivotKey, enc); err != nil {
|
|
||||||
log.Crit("Failed to store pivot block number", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadTxIndexTail retrieves the number of oldest indexed block
|
|
||||||
// whose transaction indices has been indexed.
|
|
||||||
func ReadTxIndexTail(db ethdb.KeyValueReader) *uint64 {
|
|
||||||
data, _ := db.Get(txIndexTailKey)
|
|
||||||
if len(data) != 8 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
number := binary.BigEndian.Uint64(data)
|
|
||||||
return &number
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTxIndexTail stores the number of oldest indexed block
|
|
||||||
// into database.
|
|
||||||
func WriteTxIndexTail(db ethdb.KeyValueWriter, number uint64) {
|
|
||||||
if err := db.Put(txIndexTailKey, encodeBlockNumber(number)); err != nil {
|
|
||||||
log.Crit("Failed to store the transaction index tail", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFastTxLookupLimit retrieves the tx lookup limit used in fast sync.
|
|
||||||
func ReadFastTxLookupLimit(db ethdb.KeyValueReader) *uint64 {
|
|
||||||
data, _ := db.Get(fastTxLookupLimitKey)
|
|
||||||
if len(data) != 8 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
number := binary.BigEndian.Uint64(data)
|
|
||||||
return &number
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFastTxLookupLimit stores the txlookup limit used in fast sync into database.
|
|
||||||
func WriteFastTxLookupLimit(db ethdb.KeyValueWriter, number uint64) {
|
|
||||||
if err := db.Put(fastTxLookupLimitKey, encodeBlockNumber(number)); err != nil {
|
|
||||||
log.Crit("Failed to store transaction lookup limit for fast sync", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeaderRange returns the rlp-encoded headers, starting at 'number', and going
|
|
||||||
// backwards towards genesis. This method assumes that the caller already has
|
|
||||||
// placed a cap on count, to prevent DoS issues.
|
|
||||||
// Since this method operates in head-towards-genesis mode, it will return an empty
|
|
||||||
// slice in case the head ('number') is missing. Hence, the caller must ensure that
|
|
||||||
// the head ('number') argument is actually an existing header.
|
|
||||||
//
|
|
||||||
// N.B: Since the input is a number, as opposed to a hash, it's implicit that
|
|
||||||
// this method only operates on canon headers.
|
|
||||||
func ReadHeaderRange(db ethdb.Reader, number uint64, count uint64) []rlp.RawValue {
|
|
||||||
var rlpHeaders []rlp.RawValue
|
|
||||||
if count == 0 {
|
|
||||||
return rlpHeaders
|
|
||||||
}
|
|
||||||
i := number
|
|
||||||
if count-1 > number {
|
|
||||||
// It's ok to request block 0, 1 item
|
|
||||||
count = number + 1
|
|
||||||
}
|
|
||||||
limit, _ := db.Ancients()
|
|
||||||
// First read live blocks
|
|
||||||
if i >= limit {
|
|
||||||
// If we need to read live blocks, we need to figure out the hash first
|
|
||||||
hash := ReadCanonicalHash(db, number)
|
|
||||||
for ; i >= limit && count > 0; i-- {
|
|
||||||
if data, _ := db.Get(headerKey(i, hash)); len(data) > 0 {
|
|
||||||
rlpHeaders = append(rlpHeaders, data)
|
|
||||||
// Get the parent hash for next query
|
|
||||||
hash = types.HeaderParentHashFromRLP(data)
|
|
||||||
} else {
|
|
||||||
break // Maybe got moved to ancients
|
|
||||||
}
|
|
||||||
count--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
return rlpHeaders
|
|
||||||
}
|
|
||||||
// read remaining from ancients
|
|
||||||
data, err := db.AncientRange(ChainFreezerHeaderTable, i+1-count, count, 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to read headers from freezer", "err", err)
|
|
||||||
return rlpHeaders
|
|
||||||
}
|
|
||||||
if uint64(len(data)) != count {
|
|
||||||
log.Warn("Incomplete read of headers from freezer", "wanted", count, "read", len(data))
|
|
||||||
return rlpHeaders
|
|
||||||
}
|
|
||||||
// The data is on the order [h, h+1, .., n] -- reordering needed
|
|
||||||
for i := range data {
|
|
||||||
rlpHeaders = append(rlpHeaders, data[len(data)-1-i])
|
|
||||||
}
|
|
||||||
return rlpHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeaderRLP retrieves a block header in its raw RLP database encoding.
|
|
||||||
func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue {
|
|
||||||
var data []byte
|
|
||||||
db.ReadAncients(func(reader ethdb.AncientReaderOp) error {
|
|
||||||
// First try to look up the data in ancient database. Extra hash
|
|
||||||
// comparison is necessary since ancient database only maintains
|
|
||||||
// the canonical data.
|
|
||||||
data, _ = reader.Ancient(ChainFreezerHeaderTable, number)
|
|
||||||
if len(data) > 0 && crypto.Keccak256Hash(data) == hash {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If not, try reading from leveldb
|
|
||||||
data, _ = db.Get(headerKey(number, hash))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
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 {
|
|
||||||
if isCanon(db, number, hash) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if has, err := db.Has(headerKey(number, hash)); !has || err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeader retrieves the block header corresponding to the hash.
|
|
||||||
func ReadHeader(db ethdb.Reader, hash common.Hash, number uint64) *types.Header {
|
|
||||||
data := ReadHeaderRLP(db, hash, number)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
header := new(types.Header)
|
|
||||||
if err := rlp.DecodeBytes(data, header); err != nil {
|
|
||||||
log.Error("Invalid block header RLP", "hash", hash, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return 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) {
|
|
||||||
var (
|
|
||||||
hash = header.Hash()
|
|
||||||
number = header.Number.Uint64()
|
|
||||||
)
|
|
||||||
// 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)
|
|
||||||
if err := db.Put(key, data); err != nil {
|
|
||||||
log.Crit("Failed to store header", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteHeader removes all block header data associated with a hash.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteHeaderWithoutNumber removes only the block header but does not remove
|
|
||||||
// the hash to number mapping.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCanon is an internal utility method, to check whether the given number/hash
|
|
||||||
// is part of the ancient (canon) set.
|
|
||||||
func isCanon(reader ethdb.AncientReaderOp, number uint64, hash common.Hash) bool {
|
|
||||||
h, err := reader.Ancient(ChainFreezerHashTable, number)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bytes.Equal(h, hash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBodyRLP retrieves the block body (transactions and uncles) in RLP encoding.
|
|
||||||
func ReadBodyRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue {
|
|
||||||
// First try to look up the data in ancient database. Extra hash
|
|
||||||
// comparison is necessary since ancient database only maintains
|
|
||||||
// the canonical data.
|
|
||||||
var data []byte
|
|
||||||
db.ReadAncients(func(reader ethdb.AncientReaderOp) error {
|
|
||||||
// Check if the data is in ancients
|
|
||||||
if isCanon(reader, number, hash) {
|
|
||||||
data, _ = reader.Ancient(ChainFreezerBodiesTable, number)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If not, try reading from leveldb
|
|
||||||
data, _ = db.Get(blockBodyKey(number, hash))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadCanonicalBodyRLP retrieves the block body (transactions and uncles) for the canonical
|
|
||||||
// block at number, in RLP encoding.
|
|
||||||
func ReadCanonicalBodyRLP(db ethdb.Reader, number uint64) rlp.RawValue {
|
|
||||||
var data []byte
|
|
||||||
db.ReadAncients(func(reader ethdb.AncientReaderOp) error {
|
|
||||||
data, _ = reader.Ancient(ChainFreezerBodiesTable, number)
|
|
||||||
if len(data) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Block is not in ancients, read from leveldb by hash and number.
|
|
||||||
// Note: ReadCanonicalHash cannot be used here because it also
|
|
||||||
// calls ReadAncients internally.
|
|
||||||
hash, _ := db.Get(headerHashKey(number))
|
|
||||||
data, _ = db.Get(blockBodyKey(number, common.BytesToHash(hash)))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteBodyRLP stores an RLP encoded block body into the database.
|
|
||||||
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.Reader, hash common.Hash, number uint64) bool {
|
|
||||||
if isCanon(db, number, hash) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if has, err := db.Has(blockBodyKey(number, hash)); !has || err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBody retrieves the block body corresponding to the hash.
|
|
||||||
func ReadBody(db ethdb.Reader, hash common.Hash, number uint64) *types.Body {
|
|
||||||
data := ReadBodyRLP(db, hash, number)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body := new(types.Body)
|
|
||||||
if err := rlp.DecodeBytes(data, body); err != nil {
|
|
||||||
log.Error("Invalid block body RLP", "hash", hash, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteBody stores a block body into the database.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
WriteBodyRLP(db, hash, number, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBody removes all block body data associated with a hash.
|
|
||||||
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.Reader, hash common.Hash, number uint64) rlp.RawValue {
|
|
||||||
var data []byte
|
|
||||||
db.ReadAncients(func(reader ethdb.AncientReaderOp) error {
|
|
||||||
// Check if the data is in ancients
|
|
||||||
if isCanon(reader, number, hash) {
|
|
||||||
data, _ = reader.Ancient(ChainFreezerDifficultyTable, number)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If not, try reading from leveldb
|
|
||||||
data, _ = db.Get(headerTDKey(number, hash))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
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 {
|
|
||||||
data := ReadTdRLP(db, hash, number)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
td := new(big.Int)
|
|
||||||
if err := rlp.DecodeBytes(data, td); err != nil {
|
|
||||||
log.Error("Invalid block total difficulty RLP", "hash", hash, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return td
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTd stores the total difficulty of a block into the database.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if err := db.Put(headerTDKey(number, hash), data); err != nil {
|
|
||||||
log.Crit("Failed to store block total difficulty", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTd removes all block total difficulty data associated with a hash.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasReceipts verifies the existence of all the transaction receipts belonging
|
|
||||||
// to a block.
|
|
||||||
func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool {
|
|
||||||
if isCanon(db, number, hash) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if has, err := db.Has(blockReceiptsKey(number, hash)); !has || err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
var data []byte
|
|
||||||
db.ReadAncients(func(reader ethdb.AncientReaderOp) error {
|
|
||||||
// Check if the data is in ancients
|
|
||||||
if isCanon(reader, number, hash) {
|
|
||||||
data, _ = reader.Ancient(ChainFreezerReceiptTable, number)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If not, try reading from leveldb
|
|
||||||
data, _ = db.Get(blockReceiptsKey(number, hash))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
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 {
|
|
||||||
// Retrieve the flattened receipt slice
|
|
||||||
data := ReadReceiptsRLP(db, hash, number)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Convert the receipts from their storage form to their internal representation
|
|
||||||
storageReceipts := []*types.ReceiptForStorage{}
|
|
||||||
if err := rlp.DecodeBytes(data, &storageReceipts); err != nil {
|
|
||||||
log.Error("Invalid receipt array RLP", "hash", hash, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
receipts := make(types.Receipts, len(storageReceipts))
|
|
||||||
for i, storageReceipt := range storageReceipts {
|
|
||||||
receipts[i] = (*types.Receipt)(storageReceipt)
|
|
||||||
}
|
|
||||||
return receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadReceipts retrieves all the transaction receipts belonging to a block, including
|
|
||||||
// its corresponding metadata fields. If it is unable to populate these metadata
|
|
||||||
// fields then nil is returned.
|
|
||||||
//
|
|
||||||
// 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, time 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 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body := ReadBody(db, hash, number)
|
|
||||||
if body == nil {
|
|
||||||
log.Error("Missing body but have receipt", "hash", hash, "number", number)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
header := ReadHeader(db, hash, number)
|
|
||||||
|
|
||||||
var baseFee *big.Int
|
|
||||||
if header == nil {
|
|
||||||
baseFee = big.NewInt(0)
|
|
||||||
} else {
|
|
||||||
baseFee = header.BaseFee
|
|
||||||
}
|
|
||||||
// Compute effective blob gas price.
|
|
||||||
var blobGasPrice *big.Int
|
|
||||||
if header != nil && header.ExcessBlobGas != nil {
|
|
||||||
blobGasPrice = eip4844.CalcBlobFee(*header.ExcessBlobGas)
|
|
||||||
}
|
|
||||||
if err := receipts.DeriveFields(config, hash, number, time, baseFee, blobGasPrice, body.Transactions); err != nil {
|
|
||||||
log.Error("Failed to derive block receipts fields", "hash", hash, "number", number, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteReceipts stores all the transaction receipts belonging to a block.
|
|
||||||
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 {
|
|
||||||
storageReceipts[i] = (*types.ReceiptForStorage)(receipt)
|
|
||||||
}
|
|
||||||
bytes, err := rlp.EncodeToBytes(storageReceipts)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to encode block receipts", "err", err)
|
|
||||||
}
|
|
||||||
// Store the flattened receipt slice
|
|
||||||
if err := db.Put(blockReceiptsKey(number, hash), bytes); err != nil {
|
|
||||||
log.Crit("Failed to store block receipts", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteReceipts removes all receipt data associated with a block hash.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// storedReceiptRLP is the storage encoding of a receipt.
|
|
||||||
// Re-definition in core/types/receipt.go.
|
|
||||||
// TODO: Re-use the existing definition.
|
|
||||||
type storedReceiptRLP struct {
|
|
||||||
PostStateOrStatus []byte
|
|
||||||
CumulativeGasUsed uint64
|
|
||||||
Logs []*types.Log
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReceiptLogs is a barebone version of ReceiptForStorage which only keeps
|
|
||||||
// the list of logs. When decoding a stored receipt into this object we
|
|
||||||
// avoid creating the bloom filter.
|
|
||||||
type receiptLogs struct {
|
|
||||||
Logs []*types.Log
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRLP implements rlp.Decoder.
|
|
||||||
func (r *receiptLogs) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
var stored storedReceiptRLP
|
|
||||||
if err := s.Decode(&stored); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.Logs = stored.Logs
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveLogFields fills the logs in receiptLogs with information such as block number, txhash, etc.
|
|
||||||
func deriveLogFields(receipts []*receiptLogs, hash common.Hash, number uint64, txs types.Transactions) error {
|
|
||||||
logIndex := uint(0)
|
|
||||||
if len(txs) != len(receipts) {
|
|
||||||
return errors.New("transaction and receipt count mismatch")
|
|
||||||
}
|
|
||||||
for i := 0; i < len(receipts); i++ {
|
|
||||||
txHash := txs[i].Hash()
|
|
||||||
// The derived log fields can simply be set from the block and transaction
|
|
||||||
for j := 0; j < len(receipts[i].Logs); j++ {
|
|
||||||
receipts[i].Logs[j].BlockNumber = number
|
|
||||||
receipts[i].Logs[j].BlockHash = hash
|
|
||||||
receipts[i].Logs[j].TxHash = txHash
|
|
||||||
receipts[i].Logs[j].TxIndex = uint(i)
|
|
||||||
receipts[i].Logs[j].Index = logIndex
|
|
||||||
logIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadLogs retrieves the logs for all transactions in a block. In case
|
|
||||||
// receipts is not found, a nil is returned.
|
|
||||||
// Note: ReadLogs does not derive unstored log fields.
|
|
||||||
func ReadLogs(db ethdb.Reader, hash common.Hash, number uint64) [][]*types.Log {
|
|
||||||
// Retrieve the flattened receipt slice
|
|
||||||
data := ReadReceiptsRLP(db, hash, number)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
receipts := []*receiptLogs{}
|
|
||||||
if err := rlp.DecodeBytes(data, &receipts); err != nil {
|
|
||||||
log.Error("Invalid receipt array RLP", "hash", hash, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
logs := make([][]*types.Log, len(receipts))
|
|
||||||
for i, receipt := range receipts {
|
|
||||||
logs[i] = receipt.Logs
|
|
||||||
}
|
|
||||||
return logs
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBlock retrieves an entire block corresponding to the hash, assembling it
|
|
||||||
// back from the stored header and body. If either the header or body could not
|
|
||||||
// be retrieved nil is returned.
|
|
||||||
//
|
|
||||||
// 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 {
|
|
||||||
header := ReadHeader(db, hash, number)
|
|
||||||
if header == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
body := ReadBody(db, hash, number)
|
|
||||||
if body == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return types.NewBlockWithHeader(header).WithBody(body.Transactions, body.Uncles).WithWithdrawals(body.Withdrawals)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteBlock serializes a block into the database, header and body separately.
|
|
||||||
func WriteBlock(db ethdb.KeyValueWriter, block *types.Block) {
|
|
||||||
WriteBody(db, block.Hash(), block.NumberU64(), block.Body())
|
|
||||||
WriteHeader(db, block.Header())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteAncientBlocks writes entire block data into ancient store and returns the total written size.
|
|
||||||
func WriteAncientBlocks(db ethdb.AncientWriter, blocks []*types.Block, receipts []types.Receipts, td *big.Int) (int64, error) {
|
|
||||||
var (
|
|
||||||
tdSum = new(big.Int).Set(td)
|
|
||||||
stReceipts []*types.ReceiptForStorage
|
|
||||||
)
|
|
||||||
return db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for i, block := range blocks {
|
|
||||||
// Convert receipts to storage format and sum up total difficulty.
|
|
||||||
stReceipts = stReceipts[:0]
|
|
||||||
for _, receipt := range receipts[i] {
|
|
||||||
stReceipts = append(stReceipts, (*types.ReceiptForStorage)(receipt))
|
|
||||||
}
|
|
||||||
header := block.Header()
|
|
||||||
if i > 0 {
|
|
||||||
tdSum.Add(tdSum, header.Difficulty)
|
|
||||||
}
|
|
||||||
if err := writeAncientBlock(op, block, header, stReceipts, tdSum); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeAncientBlock(op ethdb.AncientWriteOp, block *types.Block, header *types.Header, receipts []*types.ReceiptForStorage, td *big.Int) error {
|
|
||||||
num := block.NumberU64()
|
|
||||||
if err := op.AppendRaw(ChainFreezerHashTable, num, block.Hash().Bytes()); err != nil {
|
|
||||||
return fmt.Errorf("can't add block %d hash: %v", num, err)
|
|
||||||
}
|
|
||||||
if err := op.Append(ChainFreezerHeaderTable, num, header); err != nil {
|
|
||||||
return fmt.Errorf("can't append block header %d: %v", num, err)
|
|
||||||
}
|
|
||||||
if err := op.Append(ChainFreezerBodiesTable, num, block.Body()); err != nil {
|
|
||||||
return fmt.Errorf("can't append block body %d: %v", num, err)
|
|
||||||
}
|
|
||||||
if err := op.Append(ChainFreezerReceiptTable, num, receipts); err != nil {
|
|
||||||
return fmt.Errorf("can't append block %d receipts: %v", num, err)
|
|
||||||
}
|
|
||||||
if err := op.Append(ChainFreezerDifficultyTable, num, td); err != nil {
|
|
||||||
return fmt.Errorf("can't append block %d total difficulty: %v", num, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBlock removes all block data associated with a hash.
|
|
||||||
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
|
|
||||||
// the hash to number mapping.
|
|
||||||
func DeleteBlockWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) {
|
|
||||||
DeleteReceipts(db, hash, number)
|
|
||||||
deleteHeaderWithoutNumber(db, hash, number)
|
|
||||||
DeleteBody(db, hash, number)
|
|
||||||
DeleteTd(db, hash, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
const badBlockToKeep = 10
|
|
||||||
|
|
||||||
type badBlock struct {
|
|
||||||
Header *types.Header
|
|
||||||
Body *types.Body
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBadBlock retrieves the bad block with the corresponding block hash.
|
|
||||||
func ReadBadBlock(db ethdb.Reader, hash common.Hash) *types.Block {
|
|
||||||
blob, err := db.Get(badBlockKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var badBlocks []*badBlock
|
|
||||||
if err := rlp.DecodeBytes(blob, &badBlocks); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, bad := range badBlocks {
|
|
||||||
if bad.Header.Hash() == hash {
|
|
||||||
return types.NewBlockWithHeader(bad.Header).WithBody(bad.Body.Transactions, bad.Body.Uncles).WithWithdrawals(bad.Body.Withdrawals)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAllBadBlocks retrieves all the bad blocks in the database.
|
|
||||||
// All returned blocks are sorted in reverse order by number.
|
|
||||||
func ReadAllBadBlocks(db ethdb.Reader) []*types.Block {
|
|
||||||
blob, err := db.Get(badBlockKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var badBlocks []*badBlock
|
|
||||||
if err := rlp.DecodeBytes(blob, &badBlocks); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var blocks []*types.Block
|
|
||||||
for _, bad := range badBlocks {
|
|
||||||
blocks = append(blocks, types.NewBlockWithHeader(bad.Header).WithBody(bad.Body.Transactions, bad.Body.Uncles).WithWithdrawals(bad.Body.Withdrawals))
|
|
||||||
}
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteBadBlock serializes the bad block into the database. If the cumulated
|
|
||||||
// bad blocks exceeds the limitation, the oldest will be dropped.
|
|
||||||
func WriteBadBlock(db ethdb.KeyValueStore, block *types.Block) {
|
|
||||||
blob, err := db.Get(badBlockKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Failed to load old bad blocks", "error", err)
|
|
||||||
}
|
|
||||||
var badBlocks []*badBlock
|
|
||||||
if len(blob) > 0 {
|
|
||||||
if err := rlp.DecodeBytes(blob, &badBlocks); err != nil {
|
|
||||||
log.Crit("Failed to decode old bad blocks", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, b := range badBlocks {
|
|
||||||
if b.Header.Number.Uint64() == block.NumberU64() && b.Header.Hash() == block.Hash() {
|
|
||||||
log.Info("Skip duplicated bad block", "number", block.NumberU64(), "hash", block.Hash())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
badBlocks = append(badBlocks, &badBlock{
|
|
||||||
Header: block.Header(),
|
|
||||||
Body: block.Body(),
|
|
||||||
})
|
|
||||||
slices.SortFunc(badBlocks, func(a, b *badBlock) int {
|
|
||||||
// Note: sorting in descending number order.
|
|
||||||
return -a.Header.Number.Cmp(b.Header.Number)
|
|
||||||
})
|
|
||||||
if len(badBlocks) > badBlockToKeep {
|
|
||||||
badBlocks = badBlocks[:badBlockToKeep]
|
|
||||||
}
|
|
||||||
data, err := rlp.EncodeToBytes(badBlocks)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to encode bad blocks", "err", err)
|
|
||||||
}
|
|
||||||
if err := db.Put(badBlockKey, data); err != nil {
|
|
||||||
log.Crit("Failed to write bad blocks", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBadBlocks deletes all the bad blocks from the database
|
|
||||||
func DeleteBadBlocks(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(badBlockKey); err != nil {
|
|
||||||
log.Crit("Failed to delete bad blocks", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindCommonAncestor returns the last common ancestor of two block headers
|
|
||||||
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 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for an := a.Number.Uint64(); an < b.Number.Uint64(); {
|
|
||||||
b = ReadHeader(db, b.ParentHash, b.Number.Uint64()-1)
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for a.Hash() != b.Hash() {
|
|
||||||
a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1)
|
|
||||||
if a == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b = ReadHeader(db, b.ParentHash, b.Number.Uint64()-1)
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeadHeader returns the current canonical head header.
|
|
||||||
func ReadHeadHeader(db ethdb.Reader) *types.Header {
|
|
||||||
headHeaderHash := ReadHeadHeaderHash(db)
|
|
||||||
if headHeaderHash == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
headHeaderNumber := ReadHeaderNumber(db, headHeaderHash)
|
|
||||||
if headHeaderNumber == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ReadHeader(db, headHeaderHash, *headHeaderNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadHeadBlock returns the current canonical head block.
|
|
||||||
func ReadHeadBlock(db ethdb.Reader) *types.Block {
|
|
||||||
headBlockHash := ReadHeadBlockHash(db)
|
|
||||||
if headBlockHash == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
headBlockNumber := ReadHeaderNumber(db, headBlockHash)
|
|
||||||
if headBlockNumber == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ReadBlock(db, headBlockHash, *headBlockNumber)
|
|
||||||
}
|
|
||||||
|
|
@ -1,933 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests block header storage and retrieval operations.
|
|
||||||
func TestHeaderStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a test header to move around the database and make sure it's really new
|
|
||||||
header := &types.Header{Number: big.NewInt(42), Extra: []byte("test header")}
|
|
||||||
if entry := ReadHeader(db, header.Hash(), header.Number.Uint64()); entry != nil {
|
|
||||||
t.Fatalf("Non existent header returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Write and verify the header in the database
|
|
||||||
WriteHeader(db, header)
|
|
||||||
if entry := ReadHeader(db, header.Hash(), header.Number.Uint64()); entry == nil {
|
|
||||||
t.Fatalf("Stored header not found")
|
|
||||||
} else if entry.Hash() != header.Hash() {
|
|
||||||
t.Fatalf("Retrieved header mismatch: have %v, want %v", entry, header)
|
|
||||||
}
|
|
||||||
if entry := ReadHeaderRLP(db, header.Hash(), header.Number.Uint64()); entry == nil {
|
|
||||||
t.Fatalf("Stored header RLP not found")
|
|
||||||
} else {
|
|
||||||
hasher := sha3.NewLegacyKeccak256()
|
|
||||||
hasher.Write(entry)
|
|
||||||
|
|
||||||
if hash := common.BytesToHash(hasher.Sum(nil)); hash != header.Hash() {
|
|
||||||
t.Fatalf("Retrieved RLP header mismatch: have %v, want %v", entry, header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete the header and verify the execution
|
|
||||||
DeleteHeader(db, header.Hash(), header.Number.Uint64())
|
|
||||||
if entry := ReadHeader(db, header.Hash(), header.Number.Uint64()); entry != nil {
|
|
||||||
t.Fatalf("Deleted header returned: %v", entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests block body storage and retrieval operations.
|
|
||||||
func TestBodyStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a test body to move around the database and make sure it's really new
|
|
||||||
body := &types.Body{Uncles: []*types.Header{{Extra: []byte("test header")}}}
|
|
||||||
|
|
||||||
hasher := sha3.NewLegacyKeccak256()
|
|
||||||
rlp.Encode(hasher, body)
|
|
||||||
hash := common.BytesToHash(hasher.Sum(nil))
|
|
||||||
|
|
||||||
if entry := ReadBody(db, hash, 0); entry != nil {
|
|
||||||
t.Fatalf("Non existent body returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Write and verify the body in the database
|
|
||||||
WriteBody(db, hash, 0, body)
|
|
||||||
if entry := ReadBody(db, hash, 0); entry == nil {
|
|
||||||
t.Fatalf("Stored body not found")
|
|
||||||
} else if types.DeriveSha(types.Transactions(entry.Transactions), newTestHasher()) != types.DeriveSha(types.Transactions(body.Transactions), newTestHasher()) || types.CalcUncleHash(entry.Uncles) != types.CalcUncleHash(body.Uncles) {
|
|
||||||
t.Fatalf("Retrieved body mismatch: have %v, want %v", entry, body)
|
|
||||||
}
|
|
||||||
if entry := ReadBodyRLP(db, hash, 0); entry == nil {
|
|
||||||
t.Fatalf("Stored body RLP not found")
|
|
||||||
} else {
|
|
||||||
hasher := sha3.NewLegacyKeccak256()
|
|
||||||
hasher.Write(entry)
|
|
||||||
|
|
||||||
if calc := common.BytesToHash(hasher.Sum(nil)); calc != hash {
|
|
||||||
t.Fatalf("Retrieved RLP body mismatch: have %v, want %v", entry, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete the body and verify the execution
|
|
||||||
DeleteBody(db, hash, 0)
|
|
||||||
if entry := ReadBody(db, hash, 0); entry != nil {
|
|
||||||
t.Fatalf("Deleted body returned: %v", entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests block storage and retrieval operations.
|
|
||||||
func TestBlockStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a test block to move around the database and make sure it's really new
|
|
||||||
block := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Extra: []byte("test block"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
})
|
|
||||||
if entry := ReadBlock(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Non existent block returned: %v", entry)
|
|
||||||
}
|
|
||||||
if entry := ReadHeader(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Non existent header returned: %v", entry)
|
|
||||||
}
|
|
||||||
if entry := ReadBody(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Non existent body returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Write and verify the block in the database
|
|
||||||
WriteBlock(db, block)
|
|
||||||
if entry := ReadBlock(db, block.Hash(), block.NumberU64()); entry == nil {
|
|
||||||
t.Fatalf("Stored block not found")
|
|
||||||
} else if entry.Hash() != block.Hash() {
|
|
||||||
t.Fatalf("Retrieved block mismatch: have %v, want %v", entry, block)
|
|
||||||
}
|
|
||||||
if entry := ReadHeader(db, block.Hash(), block.NumberU64()); entry == nil {
|
|
||||||
t.Fatalf("Stored header not found")
|
|
||||||
} else if entry.Hash() != block.Header().Hash() {
|
|
||||||
t.Fatalf("Retrieved header mismatch: have %v, want %v", entry, block.Header())
|
|
||||||
}
|
|
||||||
if entry := ReadBody(db, block.Hash(), block.NumberU64()); entry == nil {
|
|
||||||
t.Fatalf("Stored body not found")
|
|
||||||
} else if types.DeriveSha(types.Transactions(entry.Transactions), newTestHasher()) != types.DeriveSha(block.Transactions(), newTestHasher()) || types.CalcUncleHash(entry.Uncles) != types.CalcUncleHash(block.Uncles()) {
|
|
||||||
t.Fatalf("Retrieved body mismatch: have %v, want %v", entry, block.Body())
|
|
||||||
}
|
|
||||||
// Delete the block and verify the execution
|
|
||||||
DeleteBlock(db, block.Hash(), block.NumberU64())
|
|
||||||
if entry := ReadBlock(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Deleted block returned: %v", entry)
|
|
||||||
}
|
|
||||||
if entry := ReadHeader(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Deleted header returned: %v", entry)
|
|
||||||
}
|
|
||||||
if entry := ReadBody(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Deleted body returned: %v", entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that partial block contents don't get reassembled into full blocks.
|
|
||||||
func TestPartialBlockStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
block := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Extra: []byte("test block"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
})
|
|
||||||
// Store a header and check that it's not recognized as a block
|
|
||||||
WriteHeader(db, block.Header())
|
|
||||||
if entry := ReadBlock(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Non existent block returned: %v", entry)
|
|
||||||
}
|
|
||||||
DeleteHeader(db, block.Hash(), block.NumberU64())
|
|
||||||
|
|
||||||
// Store a body and check that it's not recognized as a block
|
|
||||||
WriteBody(db, block.Hash(), block.NumberU64(), block.Body())
|
|
||||||
if entry := ReadBlock(db, block.Hash(), block.NumberU64()); entry != nil {
|
|
||||||
t.Fatalf("Non existent block returned: %v", entry)
|
|
||||||
}
|
|
||||||
DeleteBody(db, block.Hash(), block.NumberU64())
|
|
||||||
|
|
||||||
// Store a header and a body separately and check reassembly
|
|
||||||
WriteHeader(db, block.Header())
|
|
||||||
WriteBody(db, block.Hash(), block.NumberU64(), block.Body())
|
|
||||||
|
|
||||||
if entry := ReadBlock(db, block.Hash(), block.NumberU64()); entry == nil {
|
|
||||||
t.Fatalf("Stored block not found")
|
|
||||||
} else if entry.Hash() != block.Hash() {
|
|
||||||
t.Fatalf("Retrieved block mismatch: have %v, want %v", entry, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests block storage and retrieval operations.
|
|
||||||
func TestBadBlockStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a test block to move around the database and make sure it's really new
|
|
||||||
block := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Number: big.NewInt(1),
|
|
||||||
Extra: []byte("bad block"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
})
|
|
||||||
if entry := ReadBadBlock(db, block.Hash()); entry != nil {
|
|
||||||
t.Fatalf("Non existent block returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Write and verify the block in the database
|
|
||||||
WriteBadBlock(db, block)
|
|
||||||
if entry := ReadBadBlock(db, block.Hash()); entry == nil {
|
|
||||||
t.Fatalf("Stored block not found")
|
|
||||||
} else if entry.Hash() != block.Hash() {
|
|
||||||
t.Fatalf("Retrieved block mismatch: have %v, want %v", entry, block)
|
|
||||||
}
|
|
||||||
// Write one more bad block
|
|
||||||
blockTwo := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Number: big.NewInt(2),
|
|
||||||
Extra: []byte("bad block two"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
})
|
|
||||||
WriteBadBlock(db, blockTwo)
|
|
||||||
|
|
||||||
// Write the block one again, should be filtered out.
|
|
||||||
WriteBadBlock(db, block)
|
|
||||||
badBlocks := ReadAllBadBlocks(db)
|
|
||||||
if len(badBlocks) != 2 {
|
|
||||||
t.Fatalf("Failed to load all bad blocks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write a bunch of bad blocks, all the blocks are should sorted
|
|
||||||
// in reverse order. The extra blocks should be truncated.
|
|
||||||
for _, n := range rand.Perm(100) {
|
|
||||||
block := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Number: big.NewInt(int64(n)),
|
|
||||||
Extra: []byte("bad block"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
})
|
|
||||||
WriteBadBlock(db, block)
|
|
||||||
}
|
|
||||||
badBlocks = ReadAllBadBlocks(db)
|
|
||||||
if len(badBlocks) != badBlockToKeep {
|
|
||||||
t.Fatalf("The number of persised bad blocks in incorrect %d", len(badBlocks))
|
|
||||||
}
|
|
||||||
for i := 0; i < len(badBlocks)-1; i++ {
|
|
||||||
if badBlocks[i].NumberU64() < badBlocks[i+1].NumberU64() {
|
|
||||||
t.Fatalf("The bad blocks are not sorted #[%d](%d) < #[%d](%d)", i, i+1, badBlocks[i].NumberU64(), badBlocks[i+1].NumberU64())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all bad blocks
|
|
||||||
DeleteBadBlocks(db)
|
|
||||||
badBlocks = ReadAllBadBlocks(db)
|
|
||||||
if len(badBlocks) != 0 {
|
|
||||||
t.Fatalf("Failed to delete bad blocks")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests block total difficulty storage and retrieval operations.
|
|
||||||
func TestTdStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a test TD to move around the database and make sure it's really new
|
|
||||||
hash, td := common.Hash{}, big.NewInt(314)
|
|
||||||
if entry := ReadTd(db, hash, 0); entry != nil {
|
|
||||||
t.Fatalf("Non existent TD returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Write and verify the TD in the database
|
|
||||||
WriteTd(db, hash, 0, td)
|
|
||||||
if entry := ReadTd(db, hash, 0); entry == nil {
|
|
||||||
t.Fatalf("Stored TD not found")
|
|
||||||
} else if entry.Cmp(td) != 0 {
|
|
||||||
t.Fatalf("Retrieved TD mismatch: have %v, want %v", entry, td)
|
|
||||||
}
|
|
||||||
// Delete the TD and verify the execution
|
|
||||||
DeleteTd(db, hash, 0)
|
|
||||||
if entry := ReadTd(db, hash, 0); entry != nil {
|
|
||||||
t.Fatalf("Deleted TD returned: %v", entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that canonical numbers can be mapped to hashes and retrieved.
|
|
||||||
func TestCanonicalMappingStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a test canonical number and assigned hash to move around
|
|
||||||
hash, number := common.Hash{0: 0xff}, uint64(314)
|
|
||||||
if entry := ReadCanonicalHash(db, number); entry != (common.Hash{}) {
|
|
||||||
t.Fatalf("Non existent canonical mapping returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Write and verify the TD in the database
|
|
||||||
WriteCanonicalHash(db, hash, number)
|
|
||||||
if entry := ReadCanonicalHash(db, number); entry == (common.Hash{}) {
|
|
||||||
t.Fatalf("Stored canonical mapping not found")
|
|
||||||
} else if entry != hash {
|
|
||||||
t.Fatalf("Retrieved canonical mapping mismatch: have %v, want %v", entry, hash)
|
|
||||||
}
|
|
||||||
// Delete the TD and verify the execution
|
|
||||||
DeleteCanonicalHash(db, number)
|
|
||||||
if entry := ReadCanonicalHash(db, number); entry != (common.Hash{}) {
|
|
||||||
t.Fatalf("Deleted canonical mapping returned: %v", entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that head headers and head blocks can be assigned, individually.
|
|
||||||
func TestHeadStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
blockHead := types.NewBlockWithHeader(&types.Header{Extra: []byte("test block header")})
|
|
||||||
blockFull := types.NewBlockWithHeader(&types.Header{Extra: []byte("test block full")})
|
|
||||||
blockFast := types.NewBlockWithHeader(&types.Header{Extra: []byte("test block fast")})
|
|
||||||
|
|
||||||
// Check that no head entries are in a pristine database
|
|
||||||
if entry := ReadHeadHeaderHash(db); entry != (common.Hash{}) {
|
|
||||||
t.Fatalf("Non head header entry returned: %v", entry)
|
|
||||||
}
|
|
||||||
if entry := ReadHeadBlockHash(db); entry != (common.Hash{}) {
|
|
||||||
t.Fatalf("Non head block entry returned: %v", entry)
|
|
||||||
}
|
|
||||||
if entry := ReadHeadFastBlockHash(db); entry != (common.Hash{}) {
|
|
||||||
t.Fatalf("Non fast head block entry returned: %v", entry)
|
|
||||||
}
|
|
||||||
// Assign separate entries for the head header and block
|
|
||||||
WriteHeadHeaderHash(db, blockHead.Hash())
|
|
||||||
WriteHeadBlockHash(db, blockFull.Hash())
|
|
||||||
WriteHeadFastBlockHash(db, blockFast.Hash())
|
|
||||||
|
|
||||||
// Check that both heads are present, and different (i.e. two heads maintained)
|
|
||||||
if entry := ReadHeadHeaderHash(db); entry != blockHead.Hash() {
|
|
||||||
t.Fatalf("Head header hash mismatch: have %v, want %v", entry, blockHead.Hash())
|
|
||||||
}
|
|
||||||
if entry := ReadHeadBlockHash(db); entry != blockFull.Hash() {
|
|
||||||
t.Fatalf("Head block hash mismatch: have %v, want %v", entry, blockFull.Hash())
|
|
||||||
}
|
|
||||||
if entry := ReadHeadFastBlockHash(db); entry != blockFast.Hash() {
|
|
||||||
t.Fatalf("Fast head block hash mismatch: have %v, want %v", entry, blockFast.Hash())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that receipts associated with a single block can be stored and retrieved.
|
|
||||||
func TestBlockReceiptStorage(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a live block since we need metadata to reconstruct the receipt
|
|
||||||
tx1 := types.NewTransaction(1, common.HexToAddress("0x1"), big.NewInt(1), 1, big.NewInt(1), nil)
|
|
||||||
tx2 := types.NewTransaction(2, common.HexToAddress("0x2"), big.NewInt(2), 2, big.NewInt(2), nil)
|
|
||||||
|
|
||||||
body := &types.Body{Transactions: types.Transactions{tx1, tx2}}
|
|
||||||
|
|
||||||
// Create the two receipts to manage afterwards
|
|
||||||
receipt1 := &types.Receipt{
|
|
||||||
Status: types.ReceiptStatusFailed,
|
|
||||||
CumulativeGasUsed: 1,
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x11})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x01, 0x11})},
|
|
||||||
},
|
|
||||||
TxHash: tx1.Hash(),
|
|
||||||
ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}),
|
|
||||||
GasUsed: 111111,
|
|
||||||
}
|
|
||||||
receipt1.Bloom = types.CreateBloom(types.Receipts{receipt1})
|
|
||||||
|
|
||||||
receipt2 := &types.Receipt{
|
|
||||||
PostState: common.Hash{2}.Bytes(),
|
|
||||||
CumulativeGasUsed: 2,
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x22})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x02, 0x22})},
|
|
||||||
},
|
|
||||||
TxHash: tx2.Hash(),
|
|
||||||
ContractAddress: common.BytesToAddress([]byte{0x02, 0x22, 0x22}),
|
|
||||||
GasUsed: 222222,
|
|
||||||
}
|
|
||||||
receipt2.Bloom = types.CreateBloom(types.Receipts{receipt2})
|
|
||||||
receipts := []*types.Receipt{receipt1, receipt2}
|
|
||||||
|
|
||||||
// Check that no receipt entries are in a pristine database
|
|
||||||
hash := common.BytesToHash([]byte{0x03, 0x14})
|
|
||||||
if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 {
|
|
||||||
t.Fatalf("non existent receipts returned: %v", rs)
|
|
||||||
}
|
|
||||||
// Insert the body that corresponds to the receipts
|
|
||||||
WriteBody(db, hash, 0, body)
|
|
||||||
|
|
||||||
// Insert the receipt slice into the database and check presence
|
|
||||||
WriteReceipts(db, hash, 0, receipts)
|
|
||||||
if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) == 0 {
|
|
||||||
t.Fatalf("no receipts returned")
|
|
||||||
} else {
|
|
||||||
if err := checkReceiptsRLP(rs, receipts); err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete the body and ensure that the receipts are no longer returned (metadata can't be recomputed)
|
|
||||||
DeleteBody(db, hash, 0)
|
|
||||||
if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); rs != nil {
|
|
||||||
t.Fatalf("receipts returned when body was deleted: %v", rs)
|
|
||||||
}
|
|
||||||
// Ensure that receipts without metadata can be returned without the block body too
|
|
||||||
if err := checkReceiptsRLP(ReadRawReceipts(db, hash, 0), receipts); err != nil {
|
|
||||||
t.Fatalf(err.Error())
|
|
||||||
}
|
|
||||||
// Sanity check that body alone without the receipt is a full purge
|
|
||||||
WriteBody(db, hash, 0, body)
|
|
||||||
|
|
||||||
DeleteReceipts(db, hash, 0)
|
|
||||||
if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 {
|
|
||||||
t.Fatalf("deleted receipts returned: %v", rs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkReceiptsRLP(have, want types.Receipts) error {
|
|
||||||
if len(have) != len(want) {
|
|
||||||
return fmt.Errorf("receipts sizes mismatch: have %d, want %d", len(have), len(want))
|
|
||||||
}
|
|
||||||
for i := 0; i < len(want); i++ {
|
|
||||||
rlpHave, err := rlp.EncodeToBytes(have[i])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rlpWant, err := rlp.EncodeToBytes(want[i])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !bytes.Equal(rlpHave, rlpWant) {
|
|
||||||
return fmt.Errorf("receipt #%d: receipt mismatch: have %s, want %s", i, hex.EncodeToString(rlpHave), hex.EncodeToString(rlpWant))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAncientStorage(t *testing.T) {
|
|
||||||
// Freezer style fast import the chain.
|
|
||||||
frdir := t.TempDir()
|
|
||||||
db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create database with ancient backend")
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Create a test block
|
|
||||||
block := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Number: big.NewInt(0),
|
|
||||||
Extra: []byte("test block"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
})
|
|
||||||
// Ensure nothing non-existent will be read
|
|
||||||
hash, number := block.Hash(), block.NumberU64()
|
|
||||||
if blob := ReadHeaderRLP(db, hash, number); len(blob) > 0 {
|
|
||||||
t.Fatalf("non existent header returned")
|
|
||||||
}
|
|
||||||
if blob := ReadBodyRLP(db, hash, number); len(blob) > 0 {
|
|
||||||
t.Fatalf("non existent body returned")
|
|
||||||
}
|
|
||||||
if blob := ReadReceiptsRLP(db, hash, number); len(blob) > 0 {
|
|
||||||
t.Fatalf("non existent receipts returned")
|
|
||||||
}
|
|
||||||
if blob := ReadTdRLP(db, hash, number); len(blob) > 0 {
|
|
||||||
t.Fatalf("non existent td returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write and verify the header in the database
|
|
||||||
WriteAncientBlocks(db, []*types.Block{block}, []types.Receipts{nil}, big.NewInt(100))
|
|
||||||
|
|
||||||
if blob := ReadHeaderRLP(db, hash, number); len(blob) == 0 {
|
|
||||||
t.Fatalf("no header returned")
|
|
||||||
}
|
|
||||||
if blob := ReadBodyRLP(db, hash, number); len(blob) == 0 {
|
|
||||||
t.Fatalf("no body returned")
|
|
||||||
}
|
|
||||||
if blob := ReadReceiptsRLP(db, hash, number); len(blob) == 0 {
|
|
||||||
t.Fatalf("no receipts returned")
|
|
||||||
}
|
|
||||||
if blob := ReadTdRLP(db, hash, number); len(blob) == 0 {
|
|
||||||
t.Fatalf("no td returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a fake hash for data retrieval, nothing should be returned.
|
|
||||||
fakeHash := common.BytesToHash([]byte{0x01, 0x02, 0x03})
|
|
||||||
if blob := ReadHeaderRLP(db, fakeHash, number); len(blob) != 0 {
|
|
||||||
t.Fatalf("invalid header returned")
|
|
||||||
}
|
|
||||||
if blob := ReadBodyRLP(db, fakeHash, number); len(blob) != 0 {
|
|
||||||
t.Fatalf("invalid body returned")
|
|
||||||
}
|
|
||||||
if blob := ReadReceiptsRLP(db, fakeHash, number); len(blob) != 0 {
|
|
||||||
t.Fatalf("invalid receipts returned")
|
|
||||||
}
|
|
||||||
if blob := ReadTdRLP(db, fakeHash, number); len(blob) != 0 {
|
|
||||||
t.Fatalf("invalid td returned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCanonicalHashIteration(t *testing.T) {
|
|
||||||
var cases = []struct {
|
|
||||||
from, to uint64
|
|
||||||
limit int
|
|
||||||
expect []uint64
|
|
||||||
}{
|
|
||||||
{1, 8, 0, nil},
|
|
||||||
{1, 8, 1, []uint64{1}},
|
|
||||||
{1, 8, 10, []uint64{1, 2, 3, 4, 5, 6, 7}},
|
|
||||||
{1, 9, 10, []uint64{1, 2, 3, 4, 5, 6, 7, 8}},
|
|
||||||
{2, 9, 10, []uint64{2, 3, 4, 5, 6, 7, 8}},
|
|
||||||
{9, 10, 10, nil},
|
|
||||||
}
|
|
||||||
// Test empty db iteration
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
numbers, _ := ReadAllCanonicalHashes(db, 0, 10, 10)
|
|
||||||
if len(numbers) != 0 {
|
|
||||||
t.Fatalf("No entry should be returned to iterate an empty db")
|
|
||||||
}
|
|
||||||
// Fill database with testing data.
|
|
||||||
for i := uint64(1); i <= 8; i++ {
|
|
||||||
WriteCanonicalHash(db, common.Hash{}, i)
|
|
||||||
WriteTd(db, common.Hash{}, i, big.NewInt(10)) // Write some interferential data
|
|
||||||
}
|
|
||||||
for i, c := range cases {
|
|
||||||
numbers, _ := ReadAllCanonicalHashes(db, c.from, c.to, c.limit)
|
|
||||||
if !reflect.DeepEqual(numbers, c.expect) {
|
|
||||||
t.Fatalf("Case %d failed, want %v, got %v", i, c.expect, numbers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHashesInRange(t *testing.T) {
|
|
||||||
mkHeader := func(number, seq int) *types.Header {
|
|
||||||
h := types.Header{
|
|
||||||
Difficulty: new(big.Int),
|
|
||||||
Number: big.NewInt(int64(number)),
|
|
||||||
GasLimit: uint64(seq),
|
|
||||||
}
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
// For each number, write N versions of that particular number
|
|
||||||
total := 0
|
|
||||||
for i := 0; i < 15; i++ {
|
|
||||||
for ii := 0; ii < i; ii++ {
|
|
||||||
WriteHeader(db, mkHeader(i, ii))
|
|
||||||
total++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashesInRange(db, 10, 10)), 10; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashesInRange(db, 10, 9)), 0; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashesInRange(db, 0, 100)), total; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashesInRange(db, 9, 10)), 9+10; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashes(db, 10)), 10; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashes(db, 16)), 0; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
if have, want := len(ReadAllHashes(db, 1)), 1; have != want {
|
|
||||||
t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This measures the write speed of the WriteAncientBlocks operation.
|
|
||||||
func BenchmarkWriteAncientBlocks(b *testing.B) {
|
|
||||||
// Open freezer database.
|
|
||||||
frdir := b.TempDir()
|
|
||||||
db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("failed to create database with ancient backend")
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Create the data to insert. The blocks must have consecutive numbers, so we create
|
|
||||||
// all of them ahead of time. However, there is no need to create receipts
|
|
||||||
// individually for each block, just make one batch here and reuse it for all writes.
|
|
||||||
const batchSize = 128
|
|
||||||
const blockTxs = 20
|
|
||||||
allBlocks := makeTestBlocks(b.N, blockTxs)
|
|
||||||
batchReceipts := makeTestReceipts(batchSize, blockTxs)
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
// The benchmark loop writes batches of blocks, but note that the total block count is
|
|
||||||
// b.N. This means the resulting ns/op measurement is the time it takes to write a
|
|
||||||
// single block and its associated data.
|
|
||||||
var td = big.NewInt(55)
|
|
||||||
var totalSize int64
|
|
||||||
for i := 0; i < b.N; i += batchSize {
|
|
||||||
length := batchSize
|
|
||||||
if i+batchSize > b.N {
|
|
||||||
length = b.N - i
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks := allBlocks[i : i+length]
|
|
||||||
receipts := batchReceipts[:length]
|
|
||||||
writeSize, err := WriteAncientBlocks(db, blocks, receipts, td)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
totalSize += writeSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable MB/s reporting.
|
|
||||||
b.SetBytes(totalSize / int64(b.N))
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeTestBlocks creates fake blocks for the ancient write benchmark.
|
|
||||||
func makeTestBlocks(nblock int, txsPerBlock int) []*types.Block {
|
|
||||||
key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
signer := types.LatestSignerForChainID(big.NewInt(8))
|
|
||||||
|
|
||||||
// Create transactions.
|
|
||||||
txs := make([]*types.Transaction, txsPerBlock)
|
|
||||||
for i := 0; i < len(txs); i++ {
|
|
||||||
var err error
|
|
||||||
to := common.Address{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
|
|
||||||
txs[i], err = types.SignNewTx(key, signer, &types.LegacyTx{
|
|
||||||
Nonce: 2,
|
|
||||||
GasPrice: big.NewInt(30000),
|
|
||||||
Gas: 0x45454545,
|
|
||||||
To: &to,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the blocks.
|
|
||||||
blocks := make([]*types.Block, nblock)
|
|
||||||
for i := 0; i < nblock; i++ {
|
|
||||||
header := &types.Header{
|
|
||||||
Number: big.NewInt(int64(i)),
|
|
||||||
Extra: []byte("test block"),
|
|
||||||
}
|
|
||||||
blocks[i] = types.NewBlockWithHeader(header).WithBody(txs, nil)
|
|
||||||
blocks[i].Hash() // pre-cache the block hash
|
|
||||||
}
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeTestReceipts creates fake receipts for the ancient write benchmark.
|
|
||||||
func makeTestReceipts(n int, nPerBlock int) []types.Receipts {
|
|
||||||
receipts := make([]*types.Receipt, nPerBlock)
|
|
||||||
for i := 0; i < len(receipts); i++ {
|
|
||||||
receipts[i] = &types.Receipt{
|
|
||||||
Status: types.ReceiptStatusSuccessful,
|
|
||||||
CumulativeGasUsed: 0x888888888,
|
|
||||||
Logs: make([]*types.Log, 5),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allReceipts := make([]types.Receipts, n)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
allReceipts[i] = receipts
|
|
||||||
}
|
|
||||||
return allReceipts
|
|
||||||
}
|
|
||||||
|
|
||||||
type fullLogRLP struct {
|
|
||||||
Address common.Address
|
|
||||||
Topics []common.Hash
|
|
||||||
Data []byte
|
|
||||||
BlockNumber uint64
|
|
||||||
TxHash common.Hash
|
|
||||||
TxIndex uint
|
|
||||||
BlockHash common.Hash
|
|
||||||
Index uint
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFullLogRLP(l *types.Log) *fullLogRLP {
|
|
||||||
return &fullLogRLP{
|
|
||||||
Address: l.Address,
|
|
||||||
Topics: l.Topics,
|
|
||||||
Data: l.Data,
|
|
||||||
BlockNumber: l.BlockNumber,
|
|
||||||
TxHash: l.TxHash,
|
|
||||||
TxIndex: l.TxIndex,
|
|
||||||
BlockHash: l.BlockHash,
|
|
||||||
Index: l.Index,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that logs associated with a single block can be retrieved.
|
|
||||||
func TestReadLogs(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
// Create a live block since we need metadata to reconstruct the receipt
|
|
||||||
tx1 := types.NewTransaction(1, common.HexToAddress("0x1"), big.NewInt(1), 1, big.NewInt(1), nil)
|
|
||||||
tx2 := types.NewTransaction(2, common.HexToAddress("0x2"), big.NewInt(2), 2, big.NewInt(2), nil)
|
|
||||||
|
|
||||||
body := &types.Body{Transactions: types.Transactions{tx1, tx2}}
|
|
||||||
|
|
||||||
// Create the two receipts to manage afterwards
|
|
||||||
receipt1 := &types.Receipt{
|
|
||||||
Status: types.ReceiptStatusFailed,
|
|
||||||
CumulativeGasUsed: 1,
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x11})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x01, 0x11})},
|
|
||||||
},
|
|
||||||
TxHash: tx1.Hash(),
|
|
||||||
ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}),
|
|
||||||
GasUsed: 111111,
|
|
||||||
}
|
|
||||||
receipt1.Bloom = types.CreateBloom(types.Receipts{receipt1})
|
|
||||||
|
|
||||||
receipt2 := &types.Receipt{
|
|
||||||
PostState: common.Hash{2}.Bytes(),
|
|
||||||
CumulativeGasUsed: 2,
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x22})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x02, 0x22})},
|
|
||||||
},
|
|
||||||
TxHash: tx2.Hash(),
|
|
||||||
ContractAddress: common.BytesToAddress([]byte{0x02, 0x22, 0x22}),
|
|
||||||
GasUsed: 222222,
|
|
||||||
}
|
|
||||||
receipt2.Bloom = types.CreateBloom(types.Receipts{receipt2})
|
|
||||||
receipts := []*types.Receipt{receipt1, receipt2}
|
|
||||||
|
|
||||||
hash := common.BytesToHash([]byte{0x03, 0x14})
|
|
||||||
// Check that no receipt entries are in a pristine database
|
|
||||||
if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 {
|
|
||||||
t.Fatalf("non existent receipts returned: %v", rs)
|
|
||||||
}
|
|
||||||
// Insert the body that corresponds to the receipts
|
|
||||||
WriteBody(db, hash, 0, body)
|
|
||||||
|
|
||||||
// Insert the receipt slice into the database and check presence
|
|
||||||
WriteReceipts(db, hash, 0, receipts)
|
|
||||||
|
|
||||||
logs := ReadLogs(db, hash, 0)
|
|
||||||
if len(logs) == 0 {
|
|
||||||
t.Fatalf("no logs returned")
|
|
||||||
}
|
|
||||||
if have, want := len(logs), 2; have != want {
|
|
||||||
t.Fatalf("unexpected number of logs returned, have %d want %d", have, want)
|
|
||||||
}
|
|
||||||
if have, want := len(logs[0]), 2; have != want {
|
|
||||||
t.Fatalf("unexpected number of logs[0] returned, have %d want %d", have, want)
|
|
||||||
}
|
|
||||||
if have, want := len(logs[1]), 2; have != want {
|
|
||||||
t.Fatalf("unexpected number of logs[1] returned, have %d want %d", have, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, pr := range receipts {
|
|
||||||
for j, pl := range pr.Logs {
|
|
||||||
rlpHave, err := rlp.EncodeToBytes(newFullLogRLP(logs[i][j]))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
rlpWant, err := rlp.EncodeToBytes(newFullLogRLP(pl))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(rlpHave, rlpWant) {
|
|
||||||
t.Fatalf("receipt #%d: receipt mismatch: have %s, want %s", i, hex.EncodeToString(rlpHave), hex.EncodeToString(rlpWant))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeriveLogFields(t *testing.T) {
|
|
||||||
// Create a few transactions to have receipts for
|
|
||||||
to2 := common.HexToAddress("0x2")
|
|
||||||
to3 := common.HexToAddress("0x3")
|
|
||||||
txs := types.Transactions{
|
|
||||||
types.NewTx(&types.LegacyTx{
|
|
||||||
Nonce: 1,
|
|
||||||
Value: big.NewInt(1),
|
|
||||||
Gas: 1,
|
|
||||||
GasPrice: big.NewInt(1),
|
|
||||||
}),
|
|
||||||
types.NewTx(&types.LegacyTx{
|
|
||||||
To: &to2,
|
|
||||||
Nonce: 2,
|
|
||||||
Value: big.NewInt(2),
|
|
||||||
Gas: 2,
|
|
||||||
GasPrice: big.NewInt(2),
|
|
||||||
}),
|
|
||||||
types.NewTx(&types.AccessListTx{
|
|
||||||
To: &to3,
|
|
||||||
Nonce: 3,
|
|
||||||
Value: big.NewInt(3),
|
|
||||||
Gas: 3,
|
|
||||||
GasPrice: big.NewInt(3),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
// Create the corresponding receipts
|
|
||||||
receipts := []*receiptLogs{
|
|
||||||
{
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x11})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x01, 0x11})},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x22})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x02, 0x22})},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Logs: []*types.Log{
|
|
||||||
{Address: common.BytesToAddress([]byte{0x33})},
|
|
||||||
{Address: common.BytesToAddress([]byte{0x03, 0x33})},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive log metadata fields
|
|
||||||
number := big.NewInt(1)
|
|
||||||
hash := common.BytesToHash([]byte{0x03, 0x14})
|
|
||||||
if err := deriveLogFields(receipts, hash, number.Uint64(), txs); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over all the computed fields and check that they're correct
|
|
||||||
logIndex := uint(0)
|
|
||||||
for i := range receipts {
|
|
||||||
for j := range receipts[i].Logs {
|
|
||||||
if receipts[i].Logs[j].BlockNumber != number.Uint64() {
|
|
||||||
t.Errorf("receipts[%d].Logs[%d].BlockNumber = %d, want %d", i, j, receipts[i].Logs[j].BlockNumber, number.Uint64())
|
|
||||||
}
|
|
||||||
if receipts[i].Logs[j].BlockHash != hash {
|
|
||||||
t.Errorf("receipts[%d].Logs[%d].BlockHash = %s, want %s", i, j, receipts[i].Logs[j].BlockHash.String(), hash.String())
|
|
||||||
}
|
|
||||||
if receipts[i].Logs[j].TxHash != txs[i].Hash() {
|
|
||||||
t.Errorf("receipts[%d].Logs[%d].TxHash = %s, want %s", i, j, receipts[i].Logs[j].TxHash.String(), txs[i].Hash().String())
|
|
||||||
}
|
|
||||||
if receipts[i].Logs[j].TxIndex != uint(i) {
|
|
||||||
t.Errorf("receipts[%d].Logs[%d].TransactionIndex = %d, want %d", i, j, receipts[i].Logs[j].TxIndex, i)
|
|
||||||
}
|
|
||||||
if receipts[i].Logs[j].Index != logIndex {
|
|
||||||
t.Errorf("receipts[%d].Logs[%d].Index = %d, want %d", i, j, receipts[i].Logs[j].Index, logIndex)
|
|
||||||
}
|
|
||||||
logIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDecodeRLPLogs(b *testing.B) {
|
|
||||||
// Encoded receipts from block 0x14ee094309fbe8f70b65f45ebcc08fb33f126942d97464aad5eb91cfd1e2d269
|
|
||||||
buf, err := os.ReadFile("testdata/stored_receipts.bin")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
b.Run("ReceiptForStorage", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
var r []*types.ReceiptForStorage
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if err := rlp.DecodeBytes(buf, &r); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
b.Run("rlpLogs", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
var r []*receiptLogs
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if err := rlp.DecodeBytes(buf, &r); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeadersRLPStorage(t *testing.T) {
|
|
||||||
// Have N headers in the freezer
|
|
||||||
frdir := t.TempDir()
|
|
||||||
|
|
||||||
db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create database with ancient backend")
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
// Create blocks
|
|
||||||
var chain []*types.Block
|
|
||||||
var pHash common.Hash
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
block := types.NewBlockWithHeader(&types.Header{
|
|
||||||
Number: big.NewInt(int64(i)),
|
|
||||||
Extra: []byte("test block"),
|
|
||||||
UncleHash: types.EmptyUncleHash,
|
|
||||||
TxHash: types.EmptyTxsHash,
|
|
||||||
ReceiptHash: types.EmptyReceiptsHash,
|
|
||||||
ParentHash: pHash,
|
|
||||||
})
|
|
||||||
chain = append(chain, block)
|
|
||||||
pHash = block.Hash()
|
|
||||||
}
|
|
||||||
var receipts []types.Receipts = make([]types.Receipts, 100)
|
|
||||||
// Write first half to ancients
|
|
||||||
WriteAncientBlocks(db, chain[:50], receipts[:50], big.NewInt(100))
|
|
||||||
// Write second half to db
|
|
||||||
for i := 50; i < 100; i++ {
|
|
||||||
WriteCanonicalHash(db, chain[i].Hash(), chain[i].NumberU64())
|
|
||||||
WriteBlock(db, chain[i])
|
|
||||||
}
|
|
||||||
checkSequence := func(from, amount int) {
|
|
||||||
headersRlp := ReadHeaderRange(db, uint64(from), uint64(amount))
|
|
||||||
if have, want := len(headersRlp), amount; have != want {
|
|
||||||
t.Fatalf("have %d headers, want %d", have, want)
|
|
||||||
}
|
|
||||||
for i, headerRlp := range headersRlp {
|
|
||||||
var header types.Header
|
|
||||||
if err := rlp.DecodeBytes(headerRlp, &header); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if have, want := header.Number.Uint64(), uint64(from-i); have != want {
|
|
||||||
t.Fatalf("wrong number, have %d want %d", have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkSequence(99, 20) // Latest block and 19 parents
|
|
||||||
checkSequence(99, 50) // Latest block -> all db blocks
|
|
||||||
checkSequence(99, 51) // Latest block -> one from ancients
|
|
||||||
checkSequence(99, 52) // Latest blocks -> two from ancients
|
|
||||||
checkSequence(50, 2) // One from db, one from ancients
|
|
||||||
checkSequence(49, 1) // One from ancients
|
|
||||||
checkSequence(49, 50) // All ancient ones
|
|
||||||
checkSequence(99, 100) // All blocks
|
|
||||||
checkSequence(0, 1) // Only genesis
|
|
||||||
checkSequence(1, 1) // Only block 1
|
|
||||||
checkSequence(1, 2) // Genesis + block 1
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadTxLookupEntry retrieves the positional metadata associated with a transaction
|
|
||||||
// hash to allow retrieving the transaction or receipt by hash.
|
|
||||||
func ReadTxLookupEntry(db ethdb.Reader, hash common.Hash) *uint64 {
|
|
||||||
data, _ := db.Get(txLookupKey(hash))
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Database v6 tx lookup just stores the block number
|
|
||||||
if len(data) < common.HashLength {
|
|
||||||
number := new(big.Int).SetBytes(data).Uint64()
|
|
||||||
return &number
|
|
||||||
}
|
|
||||||
// Database v4-v5 tx lookup format just stores the hash
|
|
||||||
if len(data) == common.HashLength {
|
|
||||||
return ReadHeaderNumber(db, common.BytesToHash(data))
|
|
||||||
}
|
|
||||||
// Finally try database v3 tx lookup format
|
|
||||||
var entry LegacyTxLookupEntry
|
|
||||||
if err := rlp.DecodeBytes(data, &entry); err != nil {
|
|
||||||
log.Error("Invalid transaction lookup entry RLP", "hash", hash, "blob", data, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &entry.BlockIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeTxLookupEntry stores a positional metadata for a transaction,
|
|
||||||
// enabling hash based transaction and receipt lookups.
|
|
||||||
func writeTxLookupEntry(db ethdb.KeyValueWriter, hash common.Hash, numberBytes []byte) {
|
|
||||||
if err := db.Put(txLookupKey(hash), numberBytes); err != nil {
|
|
||||||
log.Crit("Failed to store transaction lookup entry", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTxLookupEntries is identical to WriteTxLookupEntry, but it works on
|
|
||||||
// a list of hashes
|
|
||||||
func WriteTxLookupEntries(db ethdb.KeyValueWriter, number uint64, hashes []common.Hash) {
|
|
||||||
numberBytes := new(big.Int).SetUint64(number).Bytes()
|
|
||||||
for _, hash := range hashes {
|
|
||||||
writeTxLookupEntry(db, hash, numberBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTxLookupEntriesByBlock stores a positional metadata for every transaction from
|
|
||||||
// a block, enabling hash based transaction and receipt lookups.
|
|
||||||
func WriteTxLookupEntriesByBlock(db ethdb.KeyValueWriter, block *types.Block) {
|
|
||||||
numberBytes := block.Number().Bytes()
|
|
||||||
for _, tx := range block.Transactions() {
|
|
||||||
writeTxLookupEntry(db, tx.Hash(), numberBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTxLookupEntry removes all transaction data associated with a hash.
|
|
||||||
func DeleteTxLookupEntry(db ethdb.KeyValueWriter, hash common.Hash) {
|
|
||||||
if err := db.Delete(txLookupKey(hash)); err != nil {
|
|
||||||
log.Crit("Failed to delete transaction lookup entry", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTxLookupEntries removes all transaction lookups for a given block.
|
|
||||||
func DeleteTxLookupEntries(db ethdb.KeyValueWriter, hashes []common.Hash) {
|
|
||||||
for _, hash := range hashes {
|
|
||||||
DeleteTxLookupEntry(db, 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) {
|
|
||||||
blockNumber := ReadTxLookupEntry(db, hash)
|
|
||||||
if blockNumber == nil {
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
blockHash := ReadCanonicalHash(db, *blockNumber)
|
|
||||||
if blockHash == (common.Hash{}) {
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
body := ReadBody(db, blockHash, *blockNumber)
|
|
||||||
if body == nil {
|
|
||||||
log.Error("Transaction referenced missing", "number", *blockNumber, "hash", blockHash)
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
for txIndex, tx := range body.Transactions {
|
|
||||||
if tx.Hash() == hash {
|
|
||||||
return tx, blockHash, *blockNumber, uint64(txIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Error("Transaction not found", "number", *blockNumber, "hash", blockHash, "txhash", hash)
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// Retrieve the context of the receipt based on the transaction hash
|
|
||||||
blockNumber := ReadTxLookupEntry(db, hash)
|
|
||||||
if blockNumber == nil {
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
blockHash := ReadCanonicalHash(db, *blockNumber)
|
|
||||||
if blockHash == (common.Hash{}) {
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
blockHeader := ReadHeader(db, blockHash, *blockNumber)
|
|
||||||
if blockHeader == nil {
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
// Read all the receipts from the block and return the one with the matching hash
|
|
||||||
receipts := ReadReceipts(db, blockHash, *blockNumber, blockHeader.Time, config)
|
|
||||||
for receiptIndex, receipt := range receipts {
|
|
||||||
if receipt.TxHash == hash {
|
|
||||||
return receipt, blockHash, *blockNumber, uint64(receiptIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Error("Receipt not found", "number", *blockNumber, "hash", blockHash, "txhash", hash)
|
|
||||||
return nil, common.Hash{}, 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBloomBits retrieves the compressed bloom bit vector belonging to the given
|
|
||||||
// section and bit index from the.
|
|
||||||
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.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBloombits removes all compressed bloom bits vector belonging to the
|
|
||||||
// given section range and bit index.
|
|
||||||
func DeleteBloombits(db ethdb.Database, bit uint, from uint64, to uint64) {
|
|
||||||
start, end := bloomBitsKey(bit, from, common.Hash{}), bloomBitsKey(bit, to, common.Hash{})
|
|
||||||
it := db.NewIterator(nil, start)
|
|
||||||
defer it.Release()
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
if bytes.Compare(it.Key(), end) >= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if len(it.Key()) != len(bloomBitsPrefix)+2+8+32 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
db.Delete(it.Key())
|
|
||||||
}
|
|
||||||
if it.Error() != nil {
|
|
||||||
log.Crit("Failed to delete bloom bits", "err", it.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/blocktest"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var newTestHasher = blocktest.NewHasher
|
|
||||||
|
|
||||||
// Tests that positional lookup metadata can be stored and retrieved.
|
|
||||||
func TestLookupStorage(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
writeTxLookupEntriesByBlock func(ethdb.Writer, *types.Block)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"DatabaseV6",
|
|
||||||
func(db ethdb.Writer, block *types.Block) {
|
|
||||||
WriteTxLookupEntriesByBlock(db, block)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"DatabaseV4-V5",
|
|
||||||
func(db ethdb.Writer, block *types.Block) {
|
|
||||||
for _, tx := range block.Transactions() {
|
|
||||||
db.Put(txLookupKey(tx.Hash()), block.Hash().Bytes())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"DatabaseV3",
|
|
||||||
func(db ethdb.Writer, block *types.Block) {
|
|
||||||
for index, tx := range block.Transactions() {
|
|
||||||
entry := LegacyTxLookupEntry{
|
|
||||||
BlockHash: block.Hash(),
|
|
||||||
BlockIndex: block.NumberU64(),
|
|
||||||
Index: uint64(index),
|
|
||||||
}
|
|
||||||
data, _ := rlp.EncodeToBytes(entry)
|
|
||||||
db.Put(txLookupKey(tx.Hash()), data)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
tx1 := types.NewTransaction(1, common.BytesToAddress([]byte{0x11}), big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11})
|
|
||||||
tx2 := types.NewTransaction(2, common.BytesToAddress([]byte{0x22}), big.NewInt(222), 2222, big.NewInt(22222), []byte{0x22, 0x22, 0x22})
|
|
||||||
tx3 := types.NewTransaction(3, common.BytesToAddress([]byte{0x33}), big.NewInt(333), 3333, big.NewInt(33333), []byte{0x33, 0x33, 0x33})
|
|
||||||
txs := []*types.Transaction{tx1, tx2, tx3}
|
|
||||||
|
|
||||||
block := types.NewBlock(&types.Header{Number: big.NewInt(314)}, txs, nil, nil, newTestHasher())
|
|
||||||
|
|
||||||
// Check that no transactions entries are in a pristine database
|
|
||||||
for i, tx := range txs {
|
|
||||||
if txn, _, _, _ := ReadTransaction(db, tx.Hash()); txn != nil {
|
|
||||||
t.Fatalf("tx #%d [%x]: non existent transaction returned: %v", i, tx.Hash(), txn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Insert all the transactions into the database, and verify contents
|
|
||||||
WriteCanonicalHash(db, block.Hash(), block.NumberU64())
|
|
||||||
WriteBlock(db, block)
|
|
||||||
tc.writeTxLookupEntriesByBlock(db, block)
|
|
||||||
|
|
||||||
for i, tx := range txs {
|
|
||||||
if txn, hash, number, index := ReadTransaction(db, tx.Hash()); txn == nil {
|
|
||||||
t.Fatalf("tx #%d [%x]: transaction not found", i, tx.Hash())
|
|
||||||
} else {
|
|
||||||
if hash != block.Hash() || number != block.NumberU64() || index != uint64(i) {
|
|
||||||
t.Fatalf("tx #%d [%x]: positional metadata mismatch: have %x/%d/%d, want %x/%v/%v", i, tx.Hash(), hash, number, index, block.Hash(), block.NumberU64(), i)
|
|
||||||
}
|
|
||||||
if tx.Hash() != txn.Hash() {
|
|
||||||
t.Fatalf("tx #%d [%x]: transaction mismatch: have %v, want %v", i, tx.Hash(), txn, tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete the transactions and check purge
|
|
||||||
for i, tx := range txs {
|
|
||||||
DeleteTxLookupEntry(db, tx.Hash())
|
|
||||||
if txn, _, _, _ := ReadTransaction(db, tx.Hash()); txn != nil {
|
|
||||||
t.Fatalf("tx #%d [%x]: deleted transaction returned: %v", i, tx.Hash(), txn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteBloomBits(t *testing.T) {
|
|
||||||
// Prepare testing data
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
for i := uint(0); i < 2; i++ {
|
|
||||||
for s := uint64(0); s < 2; s++ {
|
|
||||||
WriteBloomBits(db, i, s, params.MainnetGenesisHash, []byte{0x01, 0x02})
|
|
||||||
WriteBloomBits(db, i, s, params.SepoliaGenesisHash, []byte{0x01, 0x02})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check := func(bit uint, section uint64, head common.Hash, exist bool) {
|
|
||||||
bits, _ := ReadBloomBits(db, bit, section, head)
|
|
||||||
if exist && !bytes.Equal(bits, []byte{0x01, 0x02}) {
|
|
||||||
t.Fatalf("Bloombits mismatch")
|
|
||||||
}
|
|
||||||
if !exist && len(bits) > 0 {
|
|
||||||
t.Fatalf("Bloombits should be removed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check the existence of written data.
|
|
||||||
check(0, 0, params.MainnetGenesisHash, true)
|
|
||||||
check(0, 0, params.SepoliaGenesisHash, true)
|
|
||||||
|
|
||||||
// Check the existence of deleted data.
|
|
||||||
DeleteBloombits(db, 0, 0, 1)
|
|
||||||
check(0, 0, params.MainnetGenesisHash, false)
|
|
||||||
check(0, 0, params.SepoliaGenesisHash, false)
|
|
||||||
check(0, 1, params.MainnetGenesisHash, true)
|
|
||||||
check(0, 1, params.SepoliaGenesisHash, true)
|
|
||||||
|
|
||||||
// Check the existence of deleted data.
|
|
||||||
DeleteBloombits(db, 0, 0, 2)
|
|
||||||
check(0, 0, params.MainnetGenesisHash, false)
|
|
||||||
check(0, 0, params.SepoliaGenesisHash, false)
|
|
||||||
check(0, 1, params.MainnetGenesisHash, false)
|
|
||||||
check(0, 1, params.SepoliaGenesisHash, false)
|
|
||||||
|
|
||||||
// Bit1 shouldn't be affect.
|
|
||||||
check(1, 0, params.MainnetGenesisHash, true)
|
|
||||||
check(1, 0, params.SepoliaGenesisHash, true)
|
|
||||||
check(1, 1, params.MainnetGenesisHash, true)
|
|
||||||
check(1, 1, params.SepoliaGenesisHash, true)
|
|
||||||
}
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadDatabaseVersion retrieves the version number of the database.
|
|
||||||
func ReadDatabaseVersion(db ethdb.KeyValueReader) *uint64 {
|
|
||||||
var version uint64
|
|
||||||
|
|
||||||
enc, _ := db.Get(databaseVersionKey)
|
|
||||||
if len(enc) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := rlp.DecodeBytes(enc, &version); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &version
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteDatabaseVersion stores the version number of the database
|
|
||||||
func WriteDatabaseVersion(db ethdb.KeyValueWriter, version uint64) {
|
|
||||||
enc, err := rlp.EncodeToBytes(version)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to encode database version", "err", err)
|
|
||||||
}
|
|
||||||
if err = db.Put(databaseVersionKey, enc); err != nil {
|
|
||||||
log.Crit("Failed to store the database version", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadChainConfig retrieves the consensus settings based on the given genesis hash.
|
|
||||||
func ReadChainConfig(db ethdb.KeyValueReader, hash common.Hash) *params.ChainConfig {
|
|
||||||
data, _ := db.Get(configKey(hash))
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var config params.ChainConfig
|
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
|
||||||
log.Error("Invalid chain config JSON", "hash", hash, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &config
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteChainConfig writes the chain config settings to the database.
|
|
||||||
func WriteChainConfig(db ethdb.KeyValueWriter, hash common.Hash, cfg *params.ChainConfig) {
|
|
||||||
if cfg == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to JSON encode chain config", "err", err)
|
|
||||||
}
|
|
||||||
if err := db.Put(configKey(hash), data); err != nil {
|
|
||||||
log.Crit("Failed to store chain config", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadGenesisStateSpec retrieves the genesis state specification based on the
|
|
||||||
// given genesis (block-)hash.
|
|
||||||
func ReadGenesisStateSpec(db ethdb.KeyValueReader, blockhash common.Hash) []byte {
|
|
||||||
data, _ := db.Get(genesisStateSpecKey(blockhash))
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteGenesisStateSpec writes the genesis state specification into the disk.
|
|
||||||
func WriteGenesisStateSpec(db ethdb.KeyValueWriter, blockhash common.Hash, data []byte) {
|
|
||||||
if err := db.Put(genesisStateSpecKey(blockhash), data); err != nil {
|
|
||||||
log.Crit("Failed to store genesis state", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// crashList is a list of unclean-shutdown-markers, for rlp-encoding to the
|
|
||||||
// database
|
|
||||||
type crashList struct {
|
|
||||||
Discarded uint64 // how many ucs have we deleted
|
|
||||||
Recent []uint64 // unix timestamps of 10 latest unclean shutdowns
|
|
||||||
}
|
|
||||||
|
|
||||||
const crashesToKeep = 10
|
|
||||||
|
|
||||||
// PushUncleanShutdownMarker appends a new unclean shutdown marker and returns
|
|
||||||
// the previous data
|
|
||||||
// - a list of timestamps
|
|
||||||
// - a count of how many old unclean-shutdowns have been discarded
|
|
||||||
func PushUncleanShutdownMarker(db ethdb.KeyValueStore) ([]uint64, uint64, error) {
|
|
||||||
var uncleanShutdowns crashList
|
|
||||||
// Read old data
|
|
||||||
if data, err := db.Get(uncleanShutdownKey); err == nil {
|
|
||||||
if err := rlp.DecodeBytes(data, &uncleanShutdowns); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var discarded = uncleanShutdowns.Discarded
|
|
||||||
var previous = make([]uint64, len(uncleanShutdowns.Recent))
|
|
||||||
copy(previous, uncleanShutdowns.Recent)
|
|
||||||
// Add a new (but cap it)
|
|
||||||
uncleanShutdowns.Recent = append(uncleanShutdowns.Recent, uint64(time.Now().Unix()))
|
|
||||||
if count := len(uncleanShutdowns.Recent); count > crashesToKeep+1 {
|
|
||||||
numDel := count - (crashesToKeep + 1)
|
|
||||||
uncleanShutdowns.Recent = uncleanShutdowns.Recent[numDel:]
|
|
||||||
uncleanShutdowns.Discarded += uint64(numDel)
|
|
||||||
}
|
|
||||||
// And save it again
|
|
||||||
data, _ := rlp.EncodeToBytes(uncleanShutdowns)
|
|
||||||
if err := db.Put(uncleanShutdownKey, data); err != nil {
|
|
||||||
log.Warn("Failed to write unclean-shutdown marker", "err", err)
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return previous, discarded, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PopUncleanShutdownMarker removes the last unclean shutdown marker
|
|
||||||
func PopUncleanShutdownMarker(db ethdb.KeyValueStore) {
|
|
||||||
var uncleanShutdowns crashList
|
|
||||||
// Read old data
|
|
||||||
if data, err := db.Get(uncleanShutdownKey); err != nil {
|
|
||||||
log.Warn("Error reading unclean shutdown markers", "error", err)
|
|
||||||
} else if err := rlp.DecodeBytes(data, &uncleanShutdowns); err != nil {
|
|
||||||
log.Error("Error decoding unclean shutdown markers", "error", err) // Should mos def _not_ happen
|
|
||||||
}
|
|
||||||
if l := len(uncleanShutdowns.Recent); l > 0 {
|
|
||||||
uncleanShutdowns.Recent = uncleanShutdowns.Recent[:l-1]
|
|
||||||
}
|
|
||||||
data, _ := rlp.EncodeToBytes(uncleanShutdowns)
|
|
||||||
if err := db.Put(uncleanShutdownKey, data); err != nil {
|
|
||||||
log.Warn("Failed to clear unclean-shutdown marker", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUncleanShutdownMarker updates the last marker's timestamp to now.
|
|
||||||
func UpdateUncleanShutdownMarker(db ethdb.KeyValueStore) {
|
|
||||||
var uncleanShutdowns crashList
|
|
||||||
// Read old data
|
|
||||||
if data, err := db.Get(uncleanShutdownKey); err != nil {
|
|
||||||
log.Warn("Error reading unclean shutdown markers", "error", err)
|
|
||||||
} else if err := rlp.DecodeBytes(data, &uncleanShutdowns); err != nil {
|
|
||||||
log.Warn("Error decoding unclean shutdown markers", "error", err)
|
|
||||||
}
|
|
||||||
// This shouldn't happen because we push a marker on Backend instantiation
|
|
||||||
count := len(uncleanShutdowns.Recent)
|
|
||||||
if count == 0 {
|
|
||||||
log.Warn("No unclean shutdown marker to update")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uncleanShutdowns.Recent[count-1] = uint64(time.Now().Unix())
|
|
||||||
data, _ := rlp.EncodeToBytes(uncleanShutdowns)
|
|
||||||
if err := db.Put(uncleanShutdownKey, data); err != nil {
|
|
||||||
log.Warn("Failed to write unclean-shutdown marker", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadTransitionStatus retrieves the eth2 transition status from the database
|
|
||||||
func ReadTransitionStatus(db ethdb.KeyValueReader) []byte {
|
|
||||||
data, _ := db.Get(transitionStatusKey)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTransitionStatus stores the eth2 transition status to the database
|
|
||||||
func WriteTransitionStatus(db ethdb.KeyValueWriter, data []byte) {
|
|
||||||
if err := db.Put(transitionStatusKey, data); err != nil {
|
|
||||||
log.Crit("Failed to store the eth2 transition status", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadSnapshotDisabled retrieves if the snapshot maintenance is disabled.
|
|
||||||
func ReadSnapshotDisabled(db ethdb.KeyValueReader) bool {
|
|
||||||
disabled, _ := db.Has(snapshotDisabledKey)
|
|
||||||
return disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapshotDisabled stores the snapshot pause flag.
|
|
||||||
func WriteSnapshotDisabled(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Put(snapshotDisabledKey, []byte("42")); err != nil {
|
|
||||||
log.Crit("Failed to store snapshot disabled flag", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSnapshotDisabled deletes the flag keeping the snapshot maintenance disabled.
|
|
||||||
func DeleteSnapshotDisabled(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(snapshotDisabledKey); err != nil {
|
|
||||||
log.Crit("Failed to remove snapshot disabled flag", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadSnapshotRoot retrieves the root of the block whose state is contained in
|
|
||||||
// the persisted snapshot.
|
|
||||||
func ReadSnapshotRoot(db ethdb.KeyValueReader) common.Hash {
|
|
||||||
data, _ := db.Get(SnapshotRootKey)
|
|
||||||
if len(data) != common.HashLength {
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
return common.BytesToHash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapshotRoot stores the root of the block whose state is contained in
|
|
||||||
// the persisted snapshot.
|
|
||||||
func WriteSnapshotRoot(db ethdb.KeyValueWriter, root common.Hash) {
|
|
||||||
if err := db.Put(SnapshotRootKey, root[:]); err != nil {
|
|
||||||
log.Crit("Failed to store snapshot root", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSnapshotRoot deletes the hash of the block whose state is contained in
|
|
||||||
// the persisted snapshot. Since snapshots are not immutable, this method can
|
|
||||||
// be used during updates, so a crash or failure will mark the entire snapshot
|
|
||||||
// invalid.
|
|
||||||
func DeleteSnapshotRoot(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(SnapshotRootKey); err != nil {
|
|
||||||
log.Crit("Failed to remove snapshot root", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAccountSnapshot retrieves the snapshot entry of an account trie leaf.
|
|
||||||
func ReadAccountSnapshot(db ethdb.KeyValueReader, hash common.Hash) []byte {
|
|
||||||
data, _ := db.Get(accountSnapshotKey(hash))
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteAccountSnapshot stores the snapshot entry of an account trie leaf.
|
|
||||||
func WriteAccountSnapshot(db ethdb.KeyValueWriter, hash common.Hash, entry []byte) {
|
|
||||||
if err := db.Put(accountSnapshotKey(hash), entry); err != nil {
|
|
||||||
log.Crit("Failed to store account snapshot", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAccountSnapshot removes the snapshot entry of an account trie leaf.
|
|
||||||
func DeleteAccountSnapshot(db ethdb.KeyValueWriter, hash common.Hash) {
|
|
||||||
if err := db.Delete(accountSnapshotKey(hash)); err != nil {
|
|
||||||
log.Crit("Failed to delete account snapshot", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStorageSnapshot retrieves the snapshot entry of an storage trie leaf.
|
|
||||||
func ReadStorageSnapshot(db ethdb.KeyValueReader, accountHash, storageHash common.Hash) []byte {
|
|
||||||
data, _ := db.Get(storageSnapshotKey(accountHash, storageHash))
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteStorageSnapshot stores the snapshot entry of an storage trie leaf.
|
|
||||||
func WriteStorageSnapshot(db ethdb.KeyValueWriter, accountHash, storageHash common.Hash, entry []byte) {
|
|
||||||
if err := db.Put(storageSnapshotKey(accountHash, storageHash), entry); err != nil {
|
|
||||||
log.Crit("Failed to store storage snapshot", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteStorageSnapshot removes the snapshot entry of an storage trie leaf.
|
|
||||||
func DeleteStorageSnapshot(db ethdb.KeyValueWriter, accountHash, storageHash common.Hash) {
|
|
||||||
if err := db.Delete(storageSnapshotKey(accountHash, storageHash)); err != nil {
|
|
||||||
log.Crit("Failed to delete storage snapshot", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IterateStorageSnapshots returns an iterator for walking the entire storage
|
|
||||||
// space of a specific account.
|
|
||||||
func IterateStorageSnapshots(db ethdb.Iteratee, accountHash common.Hash) ethdb.Iterator {
|
|
||||||
return NewKeyLengthIterator(db.NewIterator(storageSnapshotsKey(accountHash), nil), len(SnapshotStoragePrefix)+2*common.HashLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadSnapshotJournal retrieves the serialized in-memory diff layers saved at
|
|
||||||
// the last shutdown. The blob is expected to be max a few 10s of megabytes.
|
|
||||||
func ReadSnapshotJournal(db ethdb.KeyValueReader) []byte {
|
|
||||||
data, _ := db.Get(snapshotJournalKey)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapshotJournal stores the serialized in-memory diff layers to save at
|
|
||||||
// shutdown. The blob is expected to be max a few 10s of megabytes.
|
|
||||||
func WriteSnapshotJournal(db ethdb.KeyValueWriter, journal []byte) {
|
|
||||||
if err := db.Put(snapshotJournalKey, journal); err != nil {
|
|
||||||
log.Crit("Failed to store snapshot journal", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSnapshotJournal deletes the serialized in-memory diff layers saved at
|
|
||||||
// the last shutdown
|
|
||||||
func DeleteSnapshotJournal(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(snapshotJournalKey); err != nil {
|
|
||||||
log.Crit("Failed to remove snapshot journal", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadSnapshotGenerator retrieves the serialized snapshot generator saved at
|
|
||||||
// the last shutdown.
|
|
||||||
func ReadSnapshotGenerator(db ethdb.KeyValueReader) []byte {
|
|
||||||
data, _ := db.Get(snapshotGeneratorKey)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapshotGenerator stores the serialized snapshot generator to save at
|
|
||||||
// shutdown.
|
|
||||||
func WriteSnapshotGenerator(db ethdb.KeyValueWriter, generator []byte) {
|
|
||||||
if err := db.Put(snapshotGeneratorKey, generator); err != nil {
|
|
||||||
log.Crit("Failed to store snapshot generator", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSnapshotGenerator deletes the serialized snapshot generator saved at
|
|
||||||
// the last shutdown
|
|
||||||
func DeleteSnapshotGenerator(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(snapshotGeneratorKey); err != nil {
|
|
||||||
log.Crit("Failed to remove snapshot generator", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadSnapshotRecoveryNumber retrieves the block number of the last persisted
|
|
||||||
// snapshot layer.
|
|
||||||
func ReadSnapshotRecoveryNumber(db ethdb.KeyValueReader) *uint64 {
|
|
||||||
data, _ := db.Get(snapshotRecoveryKey)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(data) != 8 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
number := binary.BigEndian.Uint64(data)
|
|
||||||
return &number
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapshotRecoveryNumber stores the block number of the last persisted
|
|
||||||
// snapshot layer.
|
|
||||||
func WriteSnapshotRecoveryNumber(db ethdb.KeyValueWriter, number uint64) {
|
|
||||||
var buf [8]byte
|
|
||||||
binary.BigEndian.PutUint64(buf[:], number)
|
|
||||||
if err := db.Put(snapshotRecoveryKey, buf[:]); err != nil {
|
|
||||||
log.Crit("Failed to store snapshot recovery number", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSnapshotRecoveryNumber deletes the block number of the last persisted
|
|
||||||
// snapshot layer.
|
|
||||||
func DeleteSnapshotRecoveryNumber(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(snapshotRecoveryKey); err != nil {
|
|
||||||
log.Crit("Failed to remove snapshot recovery number", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadSnapshotSyncStatus retrieves the serialized sync status saved at shutdown.
|
|
||||||
func ReadSnapshotSyncStatus(db ethdb.KeyValueReader) []byte {
|
|
||||||
data, _ := db.Get(snapshotSyncStatusKey)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapshotSyncStatus stores the serialized sync status to save at shutdown.
|
|
||||||
func WriteSnapshotSyncStatus(db ethdb.KeyValueWriter, status []byte) {
|
|
||||||
if err := db.Put(snapshotSyncStatusKey, status); err != nil {
|
|
||||||
log.Crit("Failed to store snapshot sync status", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadPreimage retrieves a single preimage of the provided hash.
|
|
||||||
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.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
preimageCounter.Inc(int64(len(preimages)))
|
|
||||||
preimageHitCounter.Inc(int64(len(preimages)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadCode retrieves the contract code of the provided code hash.
|
|
||||||
func ReadCode(db ethdb.KeyValueReader, hash common.Hash) []byte {
|
|
||||||
// Try with the prefixed code scheme first, if not then try with legacy
|
|
||||||
// scheme.
|
|
||||||
data := ReadCodeWithPrefix(db, hash)
|
|
||||||
if len(data) != 0 {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
data, _ = db.Get(hash.Bytes())
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadCodeWithPrefix retrieves the contract code of the provided code hash.
|
|
||||||
// The main difference between this function and ReadCode is this function
|
|
||||||
// will only check the existence with latest scheme(with prefix).
|
|
||||||
func ReadCodeWithPrefix(db ethdb.KeyValueReader, hash common.Hash) []byte {
|
|
||||||
data, _ := db.Get(codeKey(hash))
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasCode checks if the contract code corresponding to the
|
|
||||||
// provided code hash is present in the db.
|
|
||||||
func HasCode(db ethdb.KeyValueReader, hash common.Hash) bool {
|
|
||||||
// Try with the prefixed code scheme first, if not then try with legacy
|
|
||||||
// scheme.
|
|
||||||
if ok := HasCodeWithPrefix(db, hash); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
ok, _ := db.Has(hash.Bytes())
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasCodeWithPrefix checks if the contract code corresponding to the
|
|
||||||
// provided code hash is present in the db. This function will only check
|
|
||||||
// presence using the prefix-scheme.
|
|
||||||
func HasCodeWithPrefix(db ethdb.KeyValueReader, hash common.Hash) bool {
|
|
||||||
ok, _ := db.Has(codeKey(hash))
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteCode writes the provided contract code database.
|
|
||||||
func WriteCode(db ethdb.KeyValueWriter, hash common.Hash, code []byte) {
|
|
||||||
if err := db.Put(codeKey(hash), code); err != nil {
|
|
||||||
log.Crit("Failed to store contract code", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteCode deletes the specified contract code from the database.
|
|
||||||
func DeleteCode(db ethdb.KeyValueWriter, hash common.Hash) {
|
|
||||||
if err := db.Delete(codeKey(hash)); err != nil {
|
|
||||||
log.Crit("Failed to delete contract code", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateID retrieves the state id with the provided state root.
|
|
||||||
func ReadStateID(db ethdb.KeyValueReader, root common.Hash) *uint64 {
|
|
||||||
data, err := db.Get(stateIDKey(root))
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
number := binary.BigEndian.Uint64(data)
|
|
||||||
return &number
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteStateID writes the provided state lookup to database.
|
|
||||||
func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) {
|
|
||||||
var buff [8]byte
|
|
||||||
binary.BigEndian.PutUint64(buff[:], id)
|
|
||||||
if err := db.Put(stateIDKey(root), buff[:]); err != nil {
|
|
||||||
log.Crit("Failed to store state ID", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteStateID deletes the specified state lookup from the database.
|
|
||||||
func DeleteStateID(db ethdb.KeyValueWriter, root common.Hash) {
|
|
||||||
if err := db.Delete(stateIDKey(root)); err != nil {
|
|
||||||
log.Crit("Failed to delete state ID", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadPersistentStateID retrieves the id of the persistent state from the database.
|
|
||||||
func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
|
|
||||||
data, _ := db.Get(persistentStateIDKey)
|
|
||||||
if len(data) != 8 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return binary.BigEndian.Uint64(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WritePersistentStateID stores the id of the persistent state into database.
|
|
||||||
func WritePersistentStateID(db ethdb.KeyValueWriter, number uint64) {
|
|
||||||
if err := db.Put(persistentStateIDKey, encodeBlockNumber(number)); err != nil {
|
|
||||||
log.Crit("Failed to store the persistent state ID", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadTrieJournal retrieves the serialized in-memory trie nodes of layers saved at
|
|
||||||
// the last shutdown.
|
|
||||||
func ReadTrieJournal(db ethdb.KeyValueReader) []byte {
|
|
||||||
data, _ := db.Get(trieJournalKey)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTrieJournal stores the serialized in-memory trie nodes of layers to save at
|
|
||||||
// shutdown.
|
|
||||||
func WriteTrieJournal(db ethdb.KeyValueWriter, journal []byte) {
|
|
||||||
if err := db.Put(trieJournalKey, journal); err != nil {
|
|
||||||
log.Crit("Failed to store tries journal", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTrieJournal deletes the serialized in-memory trie nodes of layers saved at
|
|
||||||
// the last shutdown.
|
|
||||||
func DeleteTrieJournal(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(trieJournalKey); err != nil {
|
|
||||||
log.Crit("Failed to remove tries journal", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateHistoryMeta retrieves the metadata corresponding to the specified
|
|
||||||
// state history. Compute the position of state history in freezer by minus
|
|
||||||
// one since the id of first state history starts from one(zero for initial
|
|
||||||
// state).
|
|
||||||
func ReadStateHistoryMeta(db ethdb.AncientReaderOp, id uint64) []byte {
|
|
||||||
blob, err := db.Ancient(stateHistoryMeta, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateHistoryMetaList retrieves a batch of meta objects with the specified
|
|
||||||
// start position and count. Compute the position of state history in freezer by
|
|
||||||
// minus one since the id of first state history starts from one(zero for initial
|
|
||||||
// state).
|
|
||||||
func ReadStateHistoryMetaList(db ethdb.AncientReaderOp, start uint64, count uint64) ([][]byte, error) {
|
|
||||||
return db.AncientRange(stateHistoryMeta, start-1, count, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateAccountIndex retrieves the state root corresponding to the specified
|
|
||||||
// state history. Compute the position of state history in freezer by minus one
|
|
||||||
// since the id of first state history starts from one(zero for initial state).
|
|
||||||
func ReadStateAccountIndex(db ethdb.AncientReaderOp, id uint64) []byte {
|
|
||||||
blob, err := db.Ancient(stateHistoryAccountIndex, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateStorageIndex retrieves the state root corresponding to the specified
|
|
||||||
// state history. Compute the position of state history in freezer by minus one
|
|
||||||
// since the id of first state history starts from one(zero for initial state).
|
|
||||||
func ReadStateStorageIndex(db ethdb.AncientReaderOp, id uint64) []byte {
|
|
||||||
blob, err := db.Ancient(stateHistoryStorageIndex, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateAccountHistory retrieves the state root corresponding to the specified
|
|
||||||
// state history. Compute the position of state history in freezer by minus one
|
|
||||||
// since the id of first state history starts from one(zero for initial state).
|
|
||||||
func ReadStateAccountHistory(db ethdb.AncientReaderOp, id uint64) []byte {
|
|
||||||
blob, err := db.Ancient(stateHistoryAccountData, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateStorageHistory retrieves the state root corresponding to the specified
|
|
||||||
// state history. Compute the position of state history in freezer by minus one
|
|
||||||
// since the id of first state history starts from one(zero for initial state).
|
|
||||||
func ReadStateStorageHistory(db ethdb.AncientReaderOp, id uint64) []byte {
|
|
||||||
blob, err := db.Ancient(stateHistoryStorageData, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateHistory retrieves the state history from database with provided id.
|
|
||||||
// Compute the position of state history in freezer by minus one since the id
|
|
||||||
// of first state history starts from one(zero for initial state).
|
|
||||||
func ReadStateHistory(db ethdb.AncientReaderOp, id uint64) ([]byte, []byte, []byte, []byte, []byte, error) {
|
|
||||||
meta, err := db.Ancient(stateHistoryMeta, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, nil, nil, err
|
|
||||||
}
|
|
||||||
accountIndex, err := db.Ancient(stateHistoryAccountIndex, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, nil, nil, err
|
|
||||||
}
|
|
||||||
storageIndex, err := db.Ancient(stateHistoryStorageIndex, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, nil, nil, err
|
|
||||||
}
|
|
||||||
accountData, err := db.Ancient(stateHistoryAccountData, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, nil, nil, err
|
|
||||||
}
|
|
||||||
storageData, err := db.Ancient(stateHistoryStorageData, id-1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, nil, nil, err
|
|
||||||
}
|
|
||||||
return meta, accountIndex, storageIndex, accountData, storageData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteStateHistory writes the provided state history to database. Compute the
|
|
||||||
// position of state history in freezer by minus one since the id of first state
|
|
||||||
// history starts from one(zero for initial state).
|
|
||||||
func WriteStateHistory(db ethdb.AncientWriter, id uint64, meta []byte, accountIndex []byte, storageIndex []byte, accounts []byte, storages []byte) {
|
|
||||||
db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
op.AppendRaw(stateHistoryMeta, id-1, meta)
|
|
||||||
op.AppendRaw(stateHistoryAccountIndex, id-1, accountIndex)
|
|
||||||
op.AppendRaw(stateHistoryStorageIndex, id-1, storageIndex)
|
|
||||||
op.AppendRaw(stateHistoryAccountData, id-1, accounts)
|
|
||||||
op.AppendRaw(stateHistoryStorageData, id-1, storages)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadSkeletonSyncStatus retrieves the serialized sync status saved at shutdown.
|
|
||||||
func ReadSkeletonSyncStatus(db ethdb.KeyValueReader) []byte {
|
|
||||||
data, _ := db.Get(skeletonSyncStatusKey)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSkeletonSyncStatus stores the serialized sync status to save at shutdown.
|
|
||||||
func WriteSkeletonSyncStatus(db ethdb.KeyValueWriter, status []byte) {
|
|
||||||
if err := db.Put(skeletonSyncStatusKey, status); err != nil {
|
|
||||||
log.Crit("Failed to store skeleton sync status", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSkeletonSyncStatus deletes the serialized sync status saved at the last
|
|
||||||
// shutdown
|
|
||||||
func DeleteSkeletonSyncStatus(db ethdb.KeyValueWriter) {
|
|
||||||
if err := db.Delete(skeletonSyncStatusKey); err != nil {
|
|
||||||
log.Crit("Failed to remove skeleton sync status", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadSkeletonHeader retrieves a block header from the skeleton sync store,
|
|
||||||
func ReadSkeletonHeader(db ethdb.KeyValueReader, number uint64) *types.Header {
|
|
||||||
data, _ := db.Get(skeletonHeaderKey(number))
|
|
||||||
if len(data) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
header := new(types.Header)
|
|
||||||
if err := rlp.DecodeBytes(data, header); err != nil {
|
|
||||||
log.Error("Invalid skeleton header RLP", "number", number, "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSkeletonHeader stores a block header into the skeleton sync store.
|
|
||||||
func WriteSkeletonHeader(db ethdb.KeyValueWriter, header *types.Header) {
|
|
||||||
data, err := rlp.EncodeToBytes(header)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to RLP encode header", "err", err)
|
|
||||||
}
|
|
||||||
key := skeletonHeaderKey(header.Number.Uint64())
|
|
||||||
if err := db.Put(key, data); err != nil {
|
|
||||||
log.Crit("Failed to store skeleton header", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSkeletonHeader removes all block header data associated with a hash.
|
|
||||||
func DeleteSkeletonHeader(db ethdb.KeyValueWriter, number uint64) {
|
|
||||||
if err := db.Delete(skeletonHeaderKey(number)); err != nil {
|
|
||||||
log.Crit("Failed to delete skeleton header", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
StateSyncUnknown = uint8(0) // flags the state snap sync is unknown
|
|
||||||
StateSyncRunning = uint8(1) // flags the state snap sync is not completed yet
|
|
||||||
StateSyncFinished = uint8(2) // flags the state snap sync is completed
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadSnapSyncStatusFlag retrieves the state snap sync status flag.
|
|
||||||
func ReadSnapSyncStatusFlag(db ethdb.KeyValueReader) uint8 {
|
|
||||||
blob, err := db.Get(snapSyncStatusFlagKey)
|
|
||||||
if err != nil || len(blob) != 1 {
|
|
||||||
return StateSyncUnknown
|
|
||||||
}
|
|
||||||
return blob[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteSnapSyncStatusFlag stores the state snap sync status flag into database.
|
|
||||||
func WriteSnapSyncStatusFlag(db ethdb.KeyValueWriter, flag uint8) {
|
|
||||||
if err := db.Put(snapSyncStatusFlagKey, []byte{flag}); err != nil {
|
|
||||||
log.Crit("Failed to store sync status flag", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,347 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HashScheme is the legacy hash-based state scheme with which trie nodes are
|
|
||||||
// stored in the disk with node hash as the database key. The advantage of this
|
|
||||||
// scheme is that different versions of trie nodes can be stored in disk, which
|
|
||||||
// is very beneficial for constructing archive nodes. The drawback is it will
|
|
||||||
// store different trie nodes on the same path to different locations on the disk
|
|
||||||
// with no data locality, and it's unfriendly for designing state pruning.
|
|
||||||
//
|
|
||||||
// Now this scheme is still kept for backward compatibility, and it will be used
|
|
||||||
// for archive node and some other tries(e.g. light trie).
|
|
||||||
const HashScheme = "hash"
|
|
||||||
|
|
||||||
// PathScheme is the new path-based state scheme with which trie nodes are stored
|
|
||||||
// in the disk with node path as the database key. This scheme will only store one
|
|
||||||
// version of state data in the disk, which means that the state pruning operation
|
|
||||||
// is native. At the same time, this scheme will put adjacent trie nodes in the same
|
|
||||||
// area of the disk with good data locality property. But this scheme needs to rely
|
|
||||||
// on extra state diffs to survive deep reorg.
|
|
||||||
const PathScheme = "path"
|
|
||||||
|
|
||||||
// hasher is used to compute the sha256 hash of the provided data.
|
|
||||||
type hasher struct{ sha crypto.KeccakState }
|
|
||||||
|
|
||||||
var hasherPool = sync.Pool{
|
|
||||||
New: func() interface{} { return &hasher{sha: sha3.NewLegacyKeccak256().(crypto.KeccakState)} },
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHasher() *hasher {
|
|
||||||
return hasherPool.Get().(*hasher)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hasher) hash(data []byte) common.Hash {
|
|
||||||
return crypto.HashData(h.sha, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *hasher) release() {
|
|
||||||
hasherPool.Put(h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAccountTrieNode retrieves the account trie node and the associated node
|
|
||||||
// hash with the specified node path.
|
|
||||||
func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) ([]byte, common.Hash) {
|
|
||||||
data, err := db.Get(accountTrieNodeKey(path))
|
|
||||||
if err != nil {
|
|
||||||
return nil, common.Hash{}
|
|
||||||
}
|
|
||||||
h := newHasher()
|
|
||||||
defer h.release()
|
|
||||||
return data, h.hash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasAccountTrieNode checks the account trie node presence with the specified
|
|
||||||
// node path and the associated node hash.
|
|
||||||
func HasAccountTrieNode(db ethdb.KeyValueReader, path []byte, hash common.Hash) bool {
|
|
||||||
data, err := db.Get(accountTrieNodeKey(path))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
h := newHasher()
|
|
||||||
defer h.release()
|
|
||||||
return h.hash(data) == hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExistsAccountTrieNode checks the presence of the account trie node with the
|
|
||||||
// specified node path, regardless of the node hash.
|
|
||||||
func ExistsAccountTrieNode(db ethdb.KeyValueReader, path []byte) bool {
|
|
||||||
has, err := db.Has(accountTrieNodeKey(path))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteAccountTrieNode writes the provided account trie node into database.
|
|
||||||
func WriteAccountTrieNode(db ethdb.KeyValueWriter, path []byte, node []byte) {
|
|
||||||
if err := db.Put(accountTrieNodeKey(path), node); err != nil {
|
|
||||||
log.Crit("Failed to store account trie node", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAccountTrieNode deletes the specified account trie node from the database.
|
|
||||||
func DeleteAccountTrieNode(db ethdb.KeyValueWriter, path []byte) {
|
|
||||||
if err := db.Delete(accountTrieNodeKey(path)); err != nil {
|
|
||||||
log.Crit("Failed to delete account trie node", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStorageTrieNode retrieves the storage trie node and the associated node
|
|
||||||
// hash with the specified node path.
|
|
||||||
func ReadStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) ([]byte, common.Hash) {
|
|
||||||
data, err := db.Get(storageTrieNodeKey(accountHash, path))
|
|
||||||
if err != nil {
|
|
||||||
return nil, common.Hash{}
|
|
||||||
}
|
|
||||||
h := newHasher()
|
|
||||||
defer h.release()
|
|
||||||
return data, h.hash(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasStorageTrieNode checks the storage trie node presence with the provided
|
|
||||||
// node path and the associated node hash.
|
|
||||||
func HasStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte, hash common.Hash) bool {
|
|
||||||
data, err := db.Get(storageTrieNodeKey(accountHash, path))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
h := newHasher()
|
|
||||||
defer h.release()
|
|
||||||
return h.hash(data) == hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExistsStorageTrieNode checks the presence of the storage trie node with the
|
|
||||||
// specified account hash and node path, regardless of the node hash.
|
|
||||||
func ExistsStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path []byte) bool {
|
|
||||||
has, err := db.Has(storageTrieNodeKey(accountHash, path))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteStorageTrieNode writes the provided storage trie node into database.
|
|
||||||
func WriteStorageTrieNode(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte, node []byte) {
|
|
||||||
if err := db.Put(storageTrieNodeKey(accountHash, path), node); err != nil {
|
|
||||||
log.Crit("Failed to store storage trie node", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteStorageTrieNode deletes the specified storage trie node from the database.
|
|
||||||
func DeleteStorageTrieNode(db ethdb.KeyValueWriter, accountHash common.Hash, path []byte) {
|
|
||||||
if err := db.Delete(storageTrieNodeKey(accountHash, path)); err != nil {
|
|
||||||
log.Crit("Failed to delete storage trie node", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadLegacyTrieNode retrieves the legacy trie node with the given
|
|
||||||
// associated node hash.
|
|
||||||
func ReadLegacyTrieNode(db ethdb.KeyValueReader, hash common.Hash) []byte {
|
|
||||||
data, err := db.Get(hash.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasLegacyTrieNode checks if the trie node with the provided hash is present in db.
|
|
||||||
func HasLegacyTrieNode(db ethdb.KeyValueReader, hash common.Hash) bool {
|
|
||||||
ok, _ := db.Has(hash.Bytes())
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteLegacyTrieNode writes the provided legacy trie node to database.
|
|
||||||
func WriteLegacyTrieNode(db ethdb.KeyValueWriter, hash common.Hash, node []byte) {
|
|
||||||
if err := db.Put(hash.Bytes(), node); err != nil {
|
|
||||||
log.Crit("Failed to store legacy trie node", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteLegacyTrieNode deletes the specified legacy trie node from database.
|
|
||||||
func DeleteLegacyTrieNode(db ethdb.KeyValueWriter, hash common.Hash) {
|
|
||||||
if err := db.Delete(hash.Bytes()); err != nil {
|
|
||||||
log.Crit("Failed to delete legacy trie node", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasTrieNode checks the trie node presence with the provided node info and
|
|
||||||
// the associated node hash.
|
|
||||||
func HasTrieNode(db ethdb.KeyValueReader, owner common.Hash, path []byte, hash common.Hash, scheme string) bool {
|
|
||||||
switch scheme {
|
|
||||||
case HashScheme:
|
|
||||||
return HasLegacyTrieNode(db, hash)
|
|
||||||
case PathScheme:
|
|
||||||
if owner == (common.Hash{}) {
|
|
||||||
return HasAccountTrieNode(db, path, hash)
|
|
||||||
}
|
|
||||||
return HasStorageTrieNode(db, owner, path, hash)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unknown scheme %v", scheme))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadTrieNode retrieves the trie node from database with the provided node info
|
|
||||||
// and associated node hash.
|
|
||||||
// hashScheme-based lookup requires the following:
|
|
||||||
// - hash
|
|
||||||
//
|
|
||||||
// pathScheme-based lookup requires the following:
|
|
||||||
// - owner
|
|
||||||
// - path
|
|
||||||
func ReadTrieNode(db ethdb.KeyValueReader, owner common.Hash, path []byte, hash common.Hash, scheme string) []byte {
|
|
||||||
switch scheme {
|
|
||||||
case HashScheme:
|
|
||||||
return ReadLegacyTrieNode(db, hash)
|
|
||||||
case PathScheme:
|
|
||||||
var (
|
|
||||||
blob []byte
|
|
||||||
nHash common.Hash
|
|
||||||
)
|
|
||||||
if owner == (common.Hash{}) {
|
|
||||||
blob, nHash = ReadAccountTrieNode(db, path)
|
|
||||||
} else {
|
|
||||||
blob, nHash = ReadStorageTrieNode(db, owner, path)
|
|
||||||
}
|
|
||||||
if nHash != hash {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return blob
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unknown scheme %v", scheme))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTrieNode writes the trie node into database with the provided node info
|
|
||||||
// and associated node hash.
|
|
||||||
// hashScheme-based lookup requires the following:
|
|
||||||
// - hash
|
|
||||||
//
|
|
||||||
// pathScheme-based lookup requires the following:
|
|
||||||
// - owner
|
|
||||||
// - path
|
|
||||||
func WriteTrieNode(db ethdb.KeyValueWriter, owner common.Hash, path []byte, hash common.Hash, node []byte, scheme string) {
|
|
||||||
switch scheme {
|
|
||||||
case HashScheme:
|
|
||||||
WriteLegacyTrieNode(db, hash, node)
|
|
||||||
case PathScheme:
|
|
||||||
if owner == (common.Hash{}) {
|
|
||||||
WriteAccountTrieNode(db, path, node)
|
|
||||||
} else {
|
|
||||||
WriteStorageTrieNode(db, owner, path, node)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unknown scheme %v", scheme))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTrieNode deletes the trie node from database with the provided node info
|
|
||||||
// and associated node hash.
|
|
||||||
// hashScheme-based lookup requires the following:
|
|
||||||
// - hash
|
|
||||||
//
|
|
||||||
// pathScheme-based lookup requires the following:
|
|
||||||
// - owner
|
|
||||||
// - path
|
|
||||||
func DeleteTrieNode(db ethdb.KeyValueWriter, owner common.Hash, path []byte, hash common.Hash, scheme string) {
|
|
||||||
switch scheme {
|
|
||||||
case HashScheme:
|
|
||||||
DeleteLegacyTrieNode(db, hash)
|
|
||||||
case PathScheme:
|
|
||||||
if owner == (common.Hash{}) {
|
|
||||||
DeleteAccountTrieNode(db, path)
|
|
||||||
} else {
|
|
||||||
DeleteStorageTrieNode(db, owner, path)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unknown scheme %v", scheme))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadStateScheme reads the state scheme of persistent state, or none
|
|
||||||
// if the state is not present in database.
|
|
||||||
func ReadStateScheme(db ethdb.Reader) string {
|
|
||||||
// Check if state in path-based scheme is present
|
|
||||||
blob, _ := ReadAccountTrieNode(db, nil)
|
|
||||||
if len(blob) != 0 {
|
|
||||||
return PathScheme
|
|
||||||
}
|
|
||||||
// The root node might be deleted during the initial snap sync, check
|
|
||||||
// the persistent state id then.
|
|
||||||
if id := ReadPersistentStateID(db); id != 0 {
|
|
||||||
return PathScheme
|
|
||||||
}
|
|
||||||
// In a hash-based scheme, the genesis state is consistently stored
|
|
||||||
// on the disk. To assess the scheme of the persistent state, it
|
|
||||||
// suffices to inspect the scheme of the genesis state.
|
|
||||||
header := ReadHeader(db, ReadCanonicalHash(db, 0), 0)
|
|
||||||
if header == nil {
|
|
||||||
return "" // empty datadir
|
|
||||||
}
|
|
||||||
blob = ReadLegacyTrieNode(db, header.Root)
|
|
||||||
if len(blob) == 0 {
|
|
||||||
return "" // no state in disk
|
|
||||||
}
|
|
||||||
return HashScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseStateScheme checks if the specified state scheme is compatible with
|
|
||||||
// the stored state.
|
|
||||||
//
|
|
||||||
// - If the provided scheme is none, use the scheme consistent with persistent
|
|
||||||
// state, or fallback to hash-based scheme if state is empty.
|
|
||||||
//
|
|
||||||
// - If the provided scheme is hash, use hash-based scheme or error out if not
|
|
||||||
// compatible with persistent state scheme.
|
|
||||||
//
|
|
||||||
// - If the provided scheme is path: use path-based scheme or error out if not
|
|
||||||
// compatible with persistent state scheme.
|
|
||||||
func ParseStateScheme(provided string, disk ethdb.Database) (string, error) {
|
|
||||||
// If state scheme is not specified, use the scheme consistent
|
|
||||||
// with persistent state, or fallback to hash mode if database
|
|
||||||
// is empty.
|
|
||||||
stored := ReadStateScheme(disk)
|
|
||||||
if provided == "" {
|
|
||||||
if stored == "" {
|
|
||||||
// use default scheme for empty database, flip it when
|
|
||||||
// path mode is chosen as default
|
|
||||||
log.Info("State schema set to default", "scheme", "hash")
|
|
||||||
return HashScheme, nil
|
|
||||||
}
|
|
||||||
log.Info("State scheme set to already existing", "scheme", stored)
|
|
||||||
return stored, nil // reuse scheme of persistent scheme
|
|
||||||
}
|
|
||||||
// If state scheme is specified, ensure it's compatible with
|
|
||||||
// persistent state.
|
|
||||||
if stored == "" || provided == stored {
|
|
||||||
log.Info("State scheme set by user", "scheme", provided)
|
|
||||||
return provided, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("incompatible state scheme, stored: %s, provided: %s", stored, provided)
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import "path/filepath"
|
|
||||||
|
|
||||||
// The list of table names of chain freezer.
|
|
||||||
const (
|
|
||||||
// ChainFreezerHeaderTable indicates the name of the freezer header table.
|
|
||||||
ChainFreezerHeaderTable = "headers"
|
|
||||||
|
|
||||||
// ChainFreezerHashTable indicates the name of the freezer canonical hash table.
|
|
||||||
ChainFreezerHashTable = "hashes"
|
|
||||||
|
|
||||||
// ChainFreezerBodiesTable indicates the name of the freezer block body table.
|
|
||||||
ChainFreezerBodiesTable = "bodies"
|
|
||||||
|
|
||||||
// ChainFreezerReceiptTable indicates the name of the freezer receipts table.
|
|
||||||
ChainFreezerReceiptTable = "receipts"
|
|
||||||
|
|
||||||
// ChainFreezerDifficultyTable indicates the name of the freezer total difficulty table.
|
|
||||||
ChainFreezerDifficultyTable = "diffs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// chainFreezerNoSnappy configures whether compression is disabled for the ancient-tables.
|
|
||||||
// Hashes and difficulties don't compress well.
|
|
||||||
var chainFreezerNoSnappy = map[string]bool{
|
|
||||||
ChainFreezerHeaderTable: false,
|
|
||||||
ChainFreezerHashTable: true,
|
|
||||||
ChainFreezerBodiesTable: false,
|
|
||||||
ChainFreezerReceiptTable: false,
|
|
||||||
ChainFreezerDifficultyTable: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// stateHistoryTableSize defines the maximum size of freezer data files.
|
|
||||||
stateHistoryTableSize = 2 * 1000 * 1000 * 1000
|
|
||||||
|
|
||||||
// stateHistoryAccountIndex indicates the name of the freezer state history table.
|
|
||||||
stateHistoryMeta = "history.meta"
|
|
||||||
stateHistoryAccountIndex = "account.index"
|
|
||||||
stateHistoryStorageIndex = "storage.index"
|
|
||||||
stateHistoryAccountData = "account.data"
|
|
||||||
stateHistoryStorageData = "storage.data"
|
|
||||||
)
|
|
||||||
|
|
||||||
var stateFreezerNoSnappy = map[string]bool{
|
|
||||||
stateHistoryMeta: true,
|
|
||||||
stateHistoryAccountIndex: false,
|
|
||||||
stateHistoryStorageIndex: false,
|
|
||||||
stateHistoryAccountData: false,
|
|
||||||
stateHistoryStorageData: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The list of identifiers of ancient stores.
|
|
||||||
var (
|
|
||||||
ChainFreezerName = "chain" // the folder name of chain segment ancient store.
|
|
||||||
StateFreezerName = "state" // the folder name of reverse diff ancient store.
|
|
||||||
)
|
|
||||||
|
|
||||||
// freezers the collections of all builtin freezers.
|
|
||||||
var freezers = []string{ChainFreezerName, StateFreezerName}
|
|
||||||
|
|
||||||
// NewStateFreezer initializes the freezer for state history.
|
|
||||||
func NewStateFreezer(ancientDir string, readOnly bool) (*ResettableFreezer, error) {
|
|
||||||
return NewResettableFreezer(filepath.Join(ancientDir, StateFreezerName), "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerNoSnappy)
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tableSize struct {
|
|
||||||
name string
|
|
||||||
size common.StorageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// freezerInfo contains the basic information of the freezer.
|
|
||||||
type freezerInfo struct {
|
|
||||||
name string // The identifier of freezer
|
|
||||||
head uint64 // The number of last stored item in the freezer
|
|
||||||
tail uint64 // The number of first stored item in the freezer
|
|
||||||
sizes []tableSize // The storage size per table
|
|
||||||
}
|
|
||||||
|
|
||||||
// count returns the number of stored items in the freezer.
|
|
||||||
func (info *freezerInfo) count() uint64 {
|
|
||||||
return info.head - info.tail + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// size returns the storage size of the entire freezer.
|
|
||||||
func (info *freezerInfo) size() common.StorageSize {
|
|
||||||
var total common.StorageSize
|
|
||||||
for _, table := range info.sizes {
|
|
||||||
total += table.size
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspect(name string, order map[string]bool, reader ethdb.AncientReader) (freezerInfo, error) {
|
|
||||||
info := freezerInfo{name: name}
|
|
||||||
for t := range order {
|
|
||||||
size, err := reader.AncientSize(t)
|
|
||||||
if err != nil {
|
|
||||||
return freezerInfo{}, err
|
|
||||||
}
|
|
||||||
info.sizes = append(info.sizes, tableSize{name: t, size: common.StorageSize(size)})
|
|
||||||
}
|
|
||||||
// Retrieve the number of last stored item
|
|
||||||
ancients, err := reader.Ancients()
|
|
||||||
if err != nil {
|
|
||||||
return freezerInfo{}, err
|
|
||||||
}
|
|
||||||
info.head = ancients - 1
|
|
||||||
|
|
||||||
// Retrieve the number of first stored item
|
|
||||||
tail, err := reader.Tail()
|
|
||||||
if err != nil {
|
|
||||||
return freezerInfo{}, err
|
|
||||||
}
|
|
||||||
info.tail = tail
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// inspectFreezers inspects all freezers registered in the system.
|
|
||||||
func inspectFreezers(db ethdb.Database) ([]freezerInfo, error) {
|
|
||||||
var infos []freezerInfo
|
|
||||||
for _, freezer := range freezers {
|
|
||||||
switch freezer {
|
|
||||||
case ChainFreezerName:
|
|
||||||
info, err := inspect(ChainFreezerName, chainFreezerNoSnappy, db)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
infos = append(infos, info)
|
|
||||||
|
|
||||||
case StateFreezerName:
|
|
||||||
if ReadStateScheme(db) != PathScheme {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
datadir, err := db.AncientDatadir()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
f, err := NewStateFreezer(datadir, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
info, err := inspect(StateFreezerName, stateFreezerNoSnappy, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
infos = append(infos, info)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown freezer, supported ones: %v", freezers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return infos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InspectFreezerTable dumps out the index of a specific freezer table. The passed
|
|
||||||
// ancient indicates the path of root ancient directory where the chain freezer can
|
|
||||||
// be opened. Start and end specify the range for dumping out indexes.
|
|
||||||
// Note this function can only be used for debugging purposes.
|
|
||||||
func InspectFreezerTable(ancient string, freezerName string, tableName string, start, end int64) error {
|
|
||||||
var (
|
|
||||||
path string
|
|
||||||
tables map[string]bool
|
|
||||||
)
|
|
||||||
switch freezerName {
|
|
||||||
case ChainFreezerName:
|
|
||||||
path, tables = resolveChainFreezerDir(ancient), chainFreezerNoSnappy
|
|
||||||
case StateFreezerName:
|
|
||||||
path, tables = filepath.Join(ancient, freezerName), stateFreezerNoSnappy
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown freezer, supported ones: %v", freezers)
|
|
||||||
}
|
|
||||||
noSnappy, exist := tables[tableName]
|
|
||||||
if !exist {
|
|
||||||
var names []string
|
|
||||||
for name := range tables {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unknown table, supported ones: %v", names)
|
|
||||||
}
|
|
||||||
table, err := newFreezerTable(path, tableName, noSnappy, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
table.dumpIndexStdout(start, end)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,303 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"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/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
// chainFreezer is a wrapper of freezer with additional chain freezing feature.
|
|
||||||
// The background thread will keep moving ancient chain segments from key-value
|
|
||||||
// database to flat files for saving space on live database.
|
|
||||||
type chainFreezer struct {
|
|
||||||
threshold atomic.Uint64 // Number of recent blocks not to freeze (params.FullImmutabilityThreshold apart from tests)
|
|
||||||
|
|
||||||
*Freezer
|
|
||||||
quit chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
trigger chan chan struct{} // Manual blocking freeze trigger, test determinism
|
|
||||||
}
|
|
||||||
|
|
||||||
// newChainFreezer initializes the freezer for ancient chain data.
|
|
||||||
func newChainFreezer(datadir string, namespace string, readonly bool) (*chainFreezer, error) {
|
|
||||||
freezer, err := NewChainFreezer(datadir, namespace, readonly)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cf := chainFreezer{
|
|
||||||
Freezer: freezer,
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
trigger: make(chan chan struct{}),
|
|
||||||
}
|
|
||||||
cf.threshold.Store(params.FullImmutabilityThreshold)
|
|
||||||
return &cf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the chain freezer instance and terminates the background thread.
|
|
||||||
func (f *chainFreezer) Close() error {
|
|
||||||
select {
|
|
||||||
case <-f.quit:
|
|
||||||
default:
|
|
||||||
close(f.quit)
|
|
||||||
}
|
|
||||||
f.wg.Wait()
|
|
||||||
return f.Freezer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 *chainFreezer) freeze(db ethdb.KeyValueStore) {
|
|
||||||
var (
|
|
||||||
backoff bool
|
|
||||||
triggered chan struct{} // Used in tests
|
|
||||||
nfdb = &nofreezedb{KeyValueStore: db}
|
|
||||||
)
|
|
||||||
timer := time.NewTimer(freezerRecheckInterval)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-f.quit:
|
|
||||||
log.Info("Freezer shutting down")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if backoff {
|
|
||||||
// If we were doing a manual trigger, notify it
|
|
||||||
if triggered != nil {
|
|
||||||
triggered <- struct{}{}
|
|
||||||
triggered = nil
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
backoff = false
|
|
||||||
timer.Reset(freezerRecheckInterval)
|
|
||||||
case triggered = <-f.trigger:
|
|
||||||
backoff = false
|
|
||||||
case <-f.quit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Retrieve the freezing threshold.
|
|
||||||
hash := ReadHeadBlockHash(nfdb)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
log.Debug("Current full block hash unavailable") // new chain, empty database
|
|
||||||
backoff = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
number := ReadHeaderNumber(nfdb, hash)
|
|
||||||
threshold := f.threshold.Load()
|
|
||||||
frozen := f.frozen.Load()
|
|
||||||
switch {
|
|
||||||
case number == nil:
|
|
||||||
log.Error("Current full block number unavailable", "hash", hash)
|
|
||||||
backoff = true
|
|
||||||
continue
|
|
||||||
|
|
||||||
case *number < threshold:
|
|
||||||
log.Debug("Current full block not old enough to freeze", "number", *number, "hash", hash, "delay", threshold)
|
|
||||||
backoff = true
|
|
||||||
continue
|
|
||||||
|
|
||||||
case *number-threshold <= frozen:
|
|
||||||
log.Debug("Ancient blocks frozen already", "number", *number, "hash", hash, "frozen", frozen)
|
|
||||||
backoff = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
head := ReadHeader(nfdb, hash, *number)
|
|
||||||
if head == nil {
|
|
||||||
log.Error("Current full block unavailable", "number", *number, "hash", hash)
|
|
||||||
backoff = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seems we have data ready to be frozen, process in usable batches
|
|
||||||
var (
|
|
||||||
start = time.Now()
|
|
||||||
first, _ = f.Ancients()
|
|
||||||
limit = *number - threshold
|
|
||||||
)
|
|
||||||
if limit-first > freezerBatchLimit {
|
|
||||||
limit = first + freezerBatchLimit
|
|
||||||
}
|
|
||||||
ancients, err := f.freezeRange(nfdb, first, limit)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error in block freeze operation", "err", err)
|
|
||||||
backoff = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 i := 0; i < len(ancients); i++ {
|
|
||||||
// Always keep the genesis block in active database
|
|
||||||
if first+uint64(i) != 0 {
|
|
||||||
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 chains also and track dangling side chains
|
|
||||||
var dangling []common.Hash
|
|
||||||
frozen = f.frozen.Load() // Needs reload after during freezeRange
|
|
||||||
for number := first; number < frozen; number++ {
|
|
||||||
// Always keep the genesis block in active database
|
|
||||||
if number != 0 {
|
|
||||||
dangling = ReadAllHashes(db, number)
|
|
||||||
for _, hash := range dangling {
|
|
||||||
log.Trace("Deleting side chain", "number", number, "hash", hash)
|
|
||||||
DeleteBlock(batch, hash, number)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to delete frozen side blocks", "err", err)
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
|
|
||||||
// Step into the future and delete any dangling side chains
|
|
||||||
if frozen > 0 {
|
|
||||||
tip := frozen
|
|
||||||
for len(dangling) > 0 {
|
|
||||||
drop := make(map[common.Hash]struct{})
|
|
||||||
for _, hash := range dangling {
|
|
||||||
log.Debug("Dangling parent from Freezer", "number", tip-1, "hash", hash)
|
|
||||||
drop[hash] = struct{}{}
|
|
||||||
}
|
|
||||||
children := ReadAllHashes(db, tip)
|
|
||||||
for i := 0; i < len(children); i++ {
|
|
||||||
// Dig up the child and ensure it's dangling
|
|
||||||
child := ReadHeader(nfdb, children[i], tip)
|
|
||||||
if child == nil {
|
|
||||||
log.Error("Missing dangling header", "number", tip, "hash", children[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := drop[child.ParentHash]; !ok {
|
|
||||||
children = append(children[:i], children[i+1:]...)
|
|
||||||
i--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Delete all block data associated with the child
|
|
||||||
log.Debug("Deleting dangling block", "number", tip, "hash", children[i], "parent", child.ParentHash)
|
|
||||||
DeleteBlock(batch, children[i], tip)
|
|
||||||
}
|
|
||||||
dangling = children
|
|
||||||
tip++
|
|
||||||
}
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to delete dangling side blocks", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log something friendly for the user
|
|
||||||
context := []interface{}{
|
|
||||||
"blocks", frozen - first, "elapsed", common.PrettyDuration(time.Since(start)), "number", frozen - 1,
|
|
||||||
}
|
|
||||||
if n := len(ancients); n > 0 {
|
|
||||||
context = append(context, []interface{}{"hash", ancients[n-1]}...)
|
|
||||||
}
|
|
||||||
log.Debug("Deep froze chain segment", context...)
|
|
||||||
|
|
||||||
// Avoid database thrashing with tiny writes
|
|
||||||
if frozen-first < freezerBatchLimit {
|
|
||||||
backoff = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hashes []common.Hash, err error) {
|
|
||||||
hashes = make([]common.Hash, 0, limit-number)
|
|
||||||
|
|
||||||
_, err = f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for ; number <= limit; number++ {
|
|
||||||
// Retrieve all the components of the canonical block.
|
|
||||||
hash := ReadCanonicalHash(nfdb, number)
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
return fmt.Errorf("canonical hash missing, can't freeze block %d", number)
|
|
||||||
}
|
|
||||||
header := ReadHeaderRLP(nfdb, hash, number)
|
|
||||||
if len(header) == 0 {
|
|
||||||
return fmt.Errorf("block header missing, can't freeze block %d", number)
|
|
||||||
}
|
|
||||||
body := ReadBodyRLP(nfdb, hash, number)
|
|
||||||
if len(body) == 0 {
|
|
||||||
return fmt.Errorf("block body missing, can't freeze block %d", number)
|
|
||||||
}
|
|
||||||
receipts := ReadReceiptsRLP(nfdb, hash, number)
|
|
||||||
if len(receipts) == 0 {
|
|
||||||
return fmt.Errorf("block receipts missing, can't freeze block %d", number)
|
|
||||||
}
|
|
||||||
td := ReadTdRLP(nfdb, hash, number)
|
|
||||||
if len(td) == 0 {
|
|
||||||
return fmt.Errorf("total difficulty missing, can't freeze block %d", number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to the batch.
|
|
||||||
if err := op.AppendRaw(ChainFreezerHashTable, number, hash[:]); err != nil {
|
|
||||||
return fmt.Errorf("can't write hash to Freezer: %v", err)
|
|
||||||
}
|
|
||||||
if err := op.AppendRaw(ChainFreezerHeaderTable, number, header); err != nil {
|
|
||||||
return fmt.Errorf("can't write header to Freezer: %v", err)
|
|
||||||
}
|
|
||||||
if err := op.AppendRaw(ChainFreezerBodiesTable, number, body); err != nil {
|
|
||||||
return fmt.Errorf("can't write body to Freezer: %v", err)
|
|
||||||
}
|
|
||||||
if err := op.AppendRaw(ChainFreezerReceiptTable, number, receipts); err != nil {
|
|
||||||
return fmt.Errorf("can't write receipts to Freezer: %v", err)
|
|
||||||
}
|
|
||||||
if err := op.AppendRaw(ChainFreezerDifficultyTable, number, td); err != nil {
|
|
||||||
return fmt.Errorf("can't write td to Freezer: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hashes = append(hashes, hash)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return hashes, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,355 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/prque"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InitDatabaseFromFreezer reinitializes an empty database from a previous batch
|
|
||||||
// of frozen ancient blocks. The method iterates over all the frozen blocks and
|
|
||||||
// injects into the database the block hash->number mappings.
|
|
||||||
func InitDatabaseFromFreezer(db ethdb.Database) {
|
|
||||||
// If we can't access the freezer or it's empty, abort
|
|
||||||
frozen, err := db.Ancients()
|
|
||||||
if err != nil || frozen == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
batch = db.NewBatch()
|
|
||||||
start = time.Now()
|
|
||||||
logged = start.Add(-7 * time.Second) // Unindex during import is fast, don't double log
|
|
||||||
hash common.Hash
|
|
||||||
)
|
|
||||||
for i := uint64(0); i < frozen; {
|
|
||||||
// We read 100K hashes at a time, for a total of 3.2M
|
|
||||||
count := uint64(100_000)
|
|
||||||
if i+count > frozen {
|
|
||||||
count = frozen - i
|
|
||||||
}
|
|
||||||
data, err := db.AncientRange(ChainFreezerHashTable, i, count, 32*count)
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Failed to init database from freezer", "err", err)
|
|
||||||
}
|
|
||||||
for j, h := range data {
|
|
||||||
number := i + uint64(j)
|
|
||||||
hash = common.BytesToHash(h)
|
|
||||||
WriteHeaderNumber(batch, hash, number)
|
|
||||||
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
|
||||||
if batch.ValueSize() > ethdb.IdealBatchSize {
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to write data to db", "err", err)
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += uint64(len(data))
|
|
||||||
// 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 database from freezer", "total", frozen, "number", i, "hash", hash, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to write data to db", "err", err)
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
|
|
||||||
WriteHeadHeaderHash(db, hash)
|
|
||||||
WriteHeadFastBlockHash(db, hash)
|
|
||||||
log.Info("Initialized database from freezer", "blocks", frozen, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type blockTxHashes struct {
|
|
||||||
number uint64
|
|
||||||
hashes []common.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterateTransactions iterates over all transactions in the (canon) block
|
|
||||||
// number(s) given, and yields the hashes on a channel. If there is a signal
|
|
||||||
// received from interrupt channel, the iteration will be aborted and result
|
|
||||||
// channel will be closed.
|
|
||||||
func iterateTransactions(db ethdb.Database, from uint64, to uint64, reverse bool, interrupt chan struct{}) chan *blockTxHashes {
|
|
||||||
// One thread sequentially reads data from db
|
|
||||||
type numberRlp struct {
|
|
||||||
number uint64
|
|
||||||
rlp rlp.RawValue
|
|
||||||
}
|
|
||||||
if to == from {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
threads := to - from
|
|
||||||
if cpus := runtime.NumCPU(); threads > uint64(cpus) {
|
|
||||||
threads = uint64(cpus)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
rlpCh = make(chan *numberRlp, threads*2) // we send raw rlp over this channel
|
|
||||||
hashesCh = make(chan *blockTxHashes, threads*2) // send hashes over hashesCh
|
|
||||||
)
|
|
||||||
// lookup runs in one instance
|
|
||||||
lookup := func() {
|
|
||||||
n, end := from, to
|
|
||||||
if reverse {
|
|
||||||
n, end = to-1, from-1
|
|
||||||
}
|
|
||||||
defer close(rlpCh)
|
|
||||||
for n != end {
|
|
||||||
data := ReadCanonicalBodyRLP(db, n)
|
|
||||||
// Feed the block to the aggregator, or abort on interrupt
|
|
||||||
select {
|
|
||||||
case rlpCh <- &numberRlp{n, data}:
|
|
||||||
case <-interrupt:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if reverse {
|
|
||||||
n--
|
|
||||||
} else {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// process runs in parallel
|
|
||||||
var nThreadsAlive atomic.Int32
|
|
||||||
nThreadsAlive.Store(int32(threads))
|
|
||||||
process := func() {
|
|
||||||
defer func() {
|
|
||||||
// Last processor closes the result channel
|
|
||||||
if nThreadsAlive.Add(-1) == 0 {
|
|
||||||
close(hashesCh)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for data := range rlpCh {
|
|
||||||
var body types.Body
|
|
||||||
if err := rlp.DecodeBytes(data.rlp, &body); err != nil {
|
|
||||||
log.Warn("Failed to decode block body", "block", data.number, "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var hashes []common.Hash
|
|
||||||
for _, tx := range body.Transactions {
|
|
||||||
hashes = append(hashes, tx.Hash())
|
|
||||||
}
|
|
||||||
result := &blockTxHashes{
|
|
||||||
hashes: hashes,
|
|
||||||
number: data.number,
|
|
||||||
}
|
|
||||||
// Feed the block to the aggregator, or abort on interrupt
|
|
||||||
select {
|
|
||||||
case hashesCh <- result:
|
|
||||||
case <-interrupt:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go lookup() // start the sequential db accessor
|
|
||||||
for i := 0; i < int(threads); i++ {
|
|
||||||
go process()
|
|
||||||
}
|
|
||||||
return hashesCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexTransactions creates txlookup indices of the specified block range.
|
|
||||||
//
|
|
||||||
// This function iterates canonical chain in reverse order, it has one main advantage:
|
|
||||||
// We can write tx index tail flag periodically even without the whole indexing
|
|
||||||
// procedure is finished. So that we can resume indexing procedure next time quickly.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func indexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
|
|
||||||
// short circuit for invalid range
|
|
||||||
if from >= to {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
hashesCh = iterateTransactions(db, from, to, true, interrupt)
|
|
||||||
batch = db.NewBatch()
|
|
||||||
start = time.Now()
|
|
||||||
logged = start.Add(-7 * time.Second)
|
|
||||||
// Since we iterate in reverse, we expect the first number to come
|
|
||||||
// in to be [to-1]. Therefore, setting lastNum to means that the
|
|
||||||
// prqueue gap-evaluation will work correctly
|
|
||||||
lastNum = to
|
|
||||||
queue = prque.New[int64, *blockTxHashes](nil)
|
|
||||||
// for stats reporting
|
|
||||||
blocks, txs = 0, 0
|
|
||||||
)
|
|
||||||
for chanDelivery := range hashesCh {
|
|
||||||
// Push the delivery into the queue and process contiguous ranges.
|
|
||||||
// Since we iterate in reverse, so lower numbers have lower prio, and
|
|
||||||
// we can use the number directly as prio marker
|
|
||||||
queue.Push(chanDelivery, int64(chanDelivery.number))
|
|
||||||
for !queue.Empty() {
|
|
||||||
// If the next available item is gapped, return
|
|
||||||
if _, priority := queue.Peek(); priority != int64(lastNum-1) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// For testing
|
|
||||||
if hook != nil && !hook(lastNum-1) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Next block available, pop it off and index it
|
|
||||||
delivery := queue.PopItem()
|
|
||||||
lastNum = delivery.number
|
|
||||||
WriteTxLookupEntries(batch, delivery.number, delivery.hashes)
|
|
||||||
blocks++
|
|
||||||
txs += len(delivery.hashes)
|
|
||||||
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
|
||||||
if batch.ValueSize() > ethdb.IdealBatchSize {
|
|
||||||
WriteTxIndexTail(batch, lastNum) // Also write the tail here
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
}
|
|
||||||
// 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("Indexing transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flush the new indexing tail and the last committed data. It can also happen
|
|
||||||
// that the last batch is empty because nothing to index, but the tail has to
|
|
||||||
// be flushed anyway.
|
|
||||||
WriteTxIndexTail(batch, lastNum)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-interrupt:
|
|
||||||
log.Debug("Transaction indexing interrupted", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
default:
|
|
||||||
log.Debug("Indexed transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndexTransactions creates txlookup indices of the specified block range. The from
|
|
||||||
// is included while to is excluded.
|
|
||||||
//
|
|
||||||
// This function iterates canonical chain in reverse order, it has one main advantage:
|
|
||||||
// We can write tx index tail flag periodically even without the whole indexing
|
|
||||||
// procedure is finished. So that we can resume indexing procedure next time quickly.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func IndexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}) {
|
|
||||||
indexTransactions(db, from, to, interrupt, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexTransactionsForTesting is the internal debug version with an additional hook.
|
|
||||||
func indexTransactionsForTesting(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
|
|
||||||
indexTransactions(db, from, to, interrupt, hook)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unindexTransactions removes txlookup indices of the specified block range.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func unindexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
|
|
||||||
// short circuit for invalid range
|
|
||||||
if from >= to {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
hashesCh = iterateTransactions(db, from, to, false, interrupt)
|
|
||||||
batch = db.NewBatch()
|
|
||||||
start = time.Now()
|
|
||||||
logged = start.Add(-7 * time.Second)
|
|
||||||
// we expect the first number to come in to be [from]. Therefore, setting
|
|
||||||
// nextNum to from means that the prqueue gap-evaluation will work correctly
|
|
||||||
nextNum = from
|
|
||||||
queue = prque.New[int64, *blockTxHashes](nil)
|
|
||||||
// for stats reporting
|
|
||||||
blocks, txs = 0, 0
|
|
||||||
)
|
|
||||||
// Otherwise spin up the concurrent iterator and unindexer
|
|
||||||
for delivery := range hashesCh {
|
|
||||||
// Push the delivery into the queue and process contiguous ranges.
|
|
||||||
queue.Push(delivery, -int64(delivery.number))
|
|
||||||
for !queue.Empty() {
|
|
||||||
// If the next available item is gapped, return
|
|
||||||
if _, priority := queue.Peek(); -priority != int64(nextNum) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// For testing
|
|
||||||
if hook != nil && !hook(nextNum) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
delivery := queue.PopItem()
|
|
||||||
nextNum = delivery.number + 1
|
|
||||||
DeleteTxLookupEntries(batch, delivery.hashes)
|
|
||||||
txs += len(delivery.hashes)
|
|
||||||
blocks++
|
|
||||||
|
|
||||||
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
|
||||||
// A batch counts the size of deletion as '1', so we need to flush more
|
|
||||||
// often than that.
|
|
||||||
if blocks%1000 == 0 {
|
|
||||||
WriteTxIndexTail(batch, nextNum)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
}
|
|
||||||
// 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("Unindexing transactions", "blocks", blocks, "txs", txs, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flush the new indexing tail and the last committed data. It can also happen
|
|
||||||
// that the last batch is empty because nothing to unindex, but the tail has to
|
|
||||||
// be flushed anyway.
|
|
||||||
WriteTxIndexTail(batch, nextNum)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-interrupt:
|
|
||||||
log.Debug("Transaction unindexing interrupted", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
default:
|
|
||||||
log.Debug("Unindexed transactions", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnindexTransactions removes txlookup indices of the specified block range.
|
|
||||||
// The from is included while to is excluded.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func UnindexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}) {
|
|
||||||
unindexTransactions(db, from, to, interrupt, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unindexTransactionsForTesting is the internal debug version with an additional hook.
|
|
||||||
func unindexTransactionsForTesting(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
|
|
||||||
unindexTransactions(db, from, to, interrupt, hook)
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChainIterator(t *testing.T) {
|
|
||||||
// Construct test chain db
|
|
||||||
chainDb := NewMemoryDatabase()
|
|
||||||
|
|
||||||
var block *types.Block
|
|
||||||
var txs []*types.Transaction
|
|
||||||
to := common.BytesToAddress([]byte{0x11})
|
|
||||||
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, nil, newTestHasher()) // Empty genesis block
|
|
||||||
WriteBlock(chainDb, block)
|
|
||||||
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
|
|
||||||
for i := uint64(1); i <= 10; i++ {
|
|
||||||
var tx *types.Transaction
|
|
||||||
if i%2 == 0 {
|
|
||||||
tx = types.NewTx(&types.LegacyTx{
|
|
||||||
Nonce: i,
|
|
||||||
GasPrice: big.NewInt(11111),
|
|
||||||
Gas: 1111,
|
|
||||||
To: &to,
|
|
||||||
Value: big.NewInt(111),
|
|
||||||
Data: []byte{0x11, 0x11, 0x11},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tx = types.NewTx(&types.AccessListTx{
|
|
||||||
ChainID: big.NewInt(1337),
|
|
||||||
Nonce: i,
|
|
||||||
GasPrice: big.NewInt(11111),
|
|
||||||
Gas: 1111,
|
|
||||||
To: &to,
|
|
||||||
Value: big.NewInt(111),
|
|
||||||
Data: []byte{0x11, 0x11, 0x11},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
txs = append(txs, tx)
|
|
||||||
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newTestHasher())
|
|
||||||
WriteBlock(chainDb, block)
|
|
||||||
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
|
|
||||||
}
|
|
||||||
|
|
||||||
var cases = []struct {
|
|
||||||
from, to uint64
|
|
||||||
reverse bool
|
|
||||||
expect []int
|
|
||||||
}{
|
|
||||||
{0, 11, true, []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}},
|
|
||||||
{0, 0, true, nil},
|
|
||||||
{0, 5, true, []int{4, 3, 2, 1, 0}},
|
|
||||||
{10, 11, true, []int{10}},
|
|
||||||
{0, 11, false, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
|
|
||||||
{0, 0, false, nil},
|
|
||||||
{10, 11, false, []int{10}},
|
|
||||||
}
|
|
||||||
for i, c := range cases {
|
|
||||||
var numbers []int
|
|
||||||
hashCh := iterateTransactions(chainDb, c.from, c.to, c.reverse, nil)
|
|
||||||
if hashCh != nil {
|
|
||||||
for h := range hashCh {
|
|
||||||
numbers = append(numbers, int(h.number))
|
|
||||||
if len(h.hashes) > 0 {
|
|
||||||
if got, exp := h.hashes[0], txs[h.number-1].Hash(); got != exp {
|
|
||||||
t.Fatalf("block %d: hash wrong, got %x exp %x", h.number, got, exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !c.reverse {
|
|
||||||
sort.Ints(numbers)
|
|
||||||
} else {
|
|
||||||
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(numbers, c.expect) {
|
|
||||||
t.Fatalf("Case %d failed, visit element mismatch, want %v, got %v", i, c.expect, numbers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIndexTransactions(t *testing.T) {
|
|
||||||
// Construct test chain db
|
|
||||||
chainDb := NewMemoryDatabase()
|
|
||||||
|
|
||||||
var block *types.Block
|
|
||||||
var txs []*types.Transaction
|
|
||||||
to := common.BytesToAddress([]byte{0x11})
|
|
||||||
|
|
||||||
// Write empty genesis block
|
|
||||||
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, nil, newTestHasher())
|
|
||||||
WriteBlock(chainDb, block)
|
|
||||||
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
|
|
||||||
|
|
||||||
for i := uint64(1); i <= 10; i++ {
|
|
||||||
var tx *types.Transaction
|
|
||||||
if i%2 == 0 {
|
|
||||||
tx = types.NewTx(&types.LegacyTx{
|
|
||||||
Nonce: i,
|
|
||||||
GasPrice: big.NewInt(11111),
|
|
||||||
Gas: 1111,
|
|
||||||
To: &to,
|
|
||||||
Value: big.NewInt(111),
|
|
||||||
Data: []byte{0x11, 0x11, 0x11},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tx = types.NewTx(&types.AccessListTx{
|
|
||||||
ChainID: big.NewInt(1337),
|
|
||||||
Nonce: i,
|
|
||||||
GasPrice: big.NewInt(11111),
|
|
||||||
Gas: 1111,
|
|
||||||
To: &to,
|
|
||||||
Value: big.NewInt(111),
|
|
||||||
Data: []byte{0x11, 0x11, 0x11},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
txs = append(txs, tx)
|
|
||||||
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newTestHasher())
|
|
||||||
WriteBlock(chainDb, block)
|
|
||||||
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
|
|
||||||
}
|
|
||||||
// verify checks whether the tx indices in the range [from, to)
|
|
||||||
// is expected.
|
|
||||||
verify := func(from, to int, exist bool, tail uint64) {
|
|
||||||
for i := from; i < to; i++ {
|
|
||||||
if i == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
number := ReadTxLookupEntry(chainDb, txs[i-1].Hash())
|
|
||||||
if exist && number == nil {
|
|
||||||
t.Fatalf("Transaction index %d missing", i)
|
|
||||||
}
|
|
||||||
if !exist && number != nil {
|
|
||||||
t.Fatalf("Transaction index %d is not deleted", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
number := ReadTxIndexTail(chainDb)
|
|
||||||
if number == nil || *number != tail {
|
|
||||||
t.Fatalf("Transaction tail mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IndexTransactions(chainDb, 5, 11, nil)
|
|
||||||
verify(5, 11, true, 5)
|
|
||||||
verify(0, 5, false, 5)
|
|
||||||
|
|
||||||
IndexTransactions(chainDb, 0, 5, nil)
|
|
||||||
verify(0, 11, true, 0)
|
|
||||||
|
|
||||||
UnindexTransactions(chainDb, 0, 5, nil)
|
|
||||||
verify(5, 11, true, 5)
|
|
||||||
verify(0, 5, false, 5)
|
|
||||||
|
|
||||||
UnindexTransactions(chainDb, 5, 11, nil)
|
|
||||||
verify(0, 11, false, 11)
|
|
||||||
|
|
||||||
// Testing corner cases
|
|
||||||
signal := make(chan struct{})
|
|
||||||
var once sync.Once
|
|
||||||
indexTransactionsForTesting(chainDb, 5, 11, signal, func(n uint64) bool {
|
|
||||||
if n <= 8 {
|
|
||||||
once.Do(func() {
|
|
||||||
close(signal)
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
verify(9, 11, true, 9)
|
|
||||||
verify(0, 9, false, 9)
|
|
||||||
IndexTransactions(chainDb, 0, 9, nil)
|
|
||||||
|
|
||||||
signal = make(chan struct{})
|
|
||||||
var once2 sync.Once
|
|
||||||
unindexTransactionsForTesting(chainDb, 0, 11, signal, func(n uint64) bool {
|
|
||||||
if n >= 8 {
|
|
||||||
once2.Do(func() {
|
|
||||||
close(signal)
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
verify(8, 11, true, 8)
|
|
||||||
verify(0, 8, false, 8)
|
|
||||||
}
|
|
||||||
|
|
@ -1,666 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"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/ethdb/pebble"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// freezerdb is a database wrapper that enables freezer data retrievals.
|
|
||||||
type freezerdb struct {
|
|
||||||
ancientRoot string
|
|
||||||
ethdb.KeyValueStore
|
|
||||||
ethdb.AncientStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientDatadir returns the path of root ancient directory.
|
|
||||||
func (frdb *freezerdb) AncientDatadir() (string, error) {
|
|
||||||
return frdb.ancientRoot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.AncientStore.Close(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
if err := frdb.KeyValueStore.Close(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
if len(errs) != 0 {
|
|
||||||
return fmt.Errorf("%v", errs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Freeze is a helper method used for external testing to trigger and block until
|
|
||||||
// a freeze cycle completes, without having to sleep for a minute to trigger the
|
|
||||||
// automatic background run.
|
|
||||||
func (frdb *freezerdb) Freeze(threshold uint64) error {
|
|
||||||
if frdb.AncientStore.(*chainFreezer).readonly {
|
|
||||||
return errReadOnly
|
|
||||||
}
|
|
||||||
// Set the freezer threshold to a temporary value
|
|
||||||
defer func(old uint64) {
|
|
||||||
frdb.AncientStore.(*chainFreezer).threshold.Store(old)
|
|
||||||
}(frdb.AncientStore.(*chainFreezer).threshold.Load())
|
|
||||||
frdb.AncientStore.(*chainFreezer).threshold.Store(threshold)
|
|
||||||
|
|
||||||
// Trigger a freeze cycle and block until it's done
|
|
||||||
trigger := make(chan struct{}, 1)
|
|
||||||
frdb.AncientStore.(*chainFreezer).trigger <- trigger
|
|
||||||
<-trigger
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nofreezedb is a database wrapper that disables freezer data retrievals.
|
|
||||||
type nofreezedb struct {
|
|
||||||
ethdb.KeyValueStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientRange returns an error as we don't have a backing chain freezer.
|
|
||||||
func (db *nofreezedb) AncientRange(kind string, start, max, maxByteSize uint64) ([][]byte, error) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tail returns an error as we don't have a backing chain freezer.
|
|
||||||
func (db *nofreezedb) Tail() (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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyAncients is not supported.
|
|
||||||
func (db *nofreezedb) ModifyAncients(func(ethdb.AncientWriteOp) error) (int64, error) {
|
|
||||||
return 0, errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateHead returns an error as we don't have a backing chain freezer.
|
|
||||||
func (db *nofreezedb) TruncateHead(items uint64) (uint64, error) {
|
|
||||||
return 0, errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateTail returns an error as we don't have a backing chain freezer.
|
|
||||||
func (db *nofreezedb) TruncateTail(items uint64) (uint64, error) {
|
|
||||||
return 0, errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync returns an error as we don't have a backing chain freezer.
|
|
||||||
func (db *nofreezedb) Sync() error {
|
|
||||||
return errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *nofreezedb) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) {
|
|
||||||
// Unlike other ancient-related methods, this method does not return
|
|
||||||
// errNotSupported when invoked.
|
|
||||||
// The reason for this is that the caller might want to do several things:
|
|
||||||
// 1. Check if something is in the freezer,
|
|
||||||
// 2. If not, check leveldb.
|
|
||||||
//
|
|
||||||
// This will work, since the ancient-checks inside 'fn' will return errors,
|
|
||||||
// and the leveldb work will continue.
|
|
||||||
//
|
|
||||||
// If we instead were to return errNotSupported here, then the caller would
|
|
||||||
// have to explicitly check for that, having an extra clause to do the
|
|
||||||
// non-ancient operations.
|
|
||||||
return fn(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MigrateTable processes the entries in a given table in sequence
|
|
||||||
// converting them to a new format if they're of an old format.
|
|
||||||
func (db *nofreezedb) MigrateTable(kind string, convert convertLegacyFn) error {
|
|
||||||
return errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientDatadir returns an error as we don't have a backing chain freezer.
|
|
||||||
func (db *nofreezedb) AncientDatadir() (string, error) {
|
|
||||||
return "", errNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 &nofreezedb{KeyValueStore: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveChainFreezerDir is a helper function which resolves the absolute path
|
|
||||||
// of chain freezer by considering backward compatibility.
|
|
||||||
func resolveChainFreezerDir(ancient string) string {
|
|
||||||
// Check if the chain freezer is already present in the specified
|
|
||||||
// sub folder, if not then two possibilities:
|
|
||||||
// - chain freezer is not initialized
|
|
||||||
// - chain freezer exists in legacy location (root ancient folder)
|
|
||||||
freezer := path.Join(ancient, ChainFreezerName)
|
|
||||||
if !common.FileExist(freezer) {
|
|
||||||
if !common.FileExist(ancient) {
|
|
||||||
// The entire ancient store is not initialized, still use the sub
|
|
||||||
// folder for initialization.
|
|
||||||
} else {
|
|
||||||
// Ancient root is already initialized, then we hold the assumption
|
|
||||||
// that chain freezer is also initialized and located in root folder.
|
|
||||||
// In this case fallback to legacy location.
|
|
||||||
freezer = ancient
|
|
||||||
log.Info("Found legacy ancient chain path", "location", ancient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return freezer
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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. The passed ancient indicates the path of root ancient directory
|
|
||||||
// where the chain freezer can be opened.
|
|
||||||
func NewDatabaseWithFreezer(db ethdb.KeyValueStore, ancient string, namespace string, readonly bool) (ethdb.Database, error) {
|
|
||||||
// Create the idle freezer instance
|
|
||||||
frdb, err := newChainFreezer(resolveChainFreezerDir(ancient), namespace, readonly)
|
|
||||||
if err != nil {
|
|
||||||
printChainMetadata(db)
|
|
||||||
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 are 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 subsequently 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.
|
|
||||||
frgenesis, err := frdb.Ancient(ChainFreezerHashTable, 0)
|
|
||||||
if err != nil {
|
|
||||||
printChainMetadata(db)
|
|
||||||
return nil, fmt.Errorf("failed to retrieve genesis from ancient %v", err)
|
|
||||||
} else if !bytes.Equal(kvgenesis, frgenesis) {
|
|
||||||
printChainMetadata(db)
|
|
||||||
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 if the database has a more recent head.
|
|
||||||
if head := *ReadHeaderNumber(db, ReadHeadHeaderHash(db)); head > frozen-1 {
|
|
||||||
// Find the smallest block stored in the key-value store
|
|
||||||
// in range of [frozen, head]
|
|
||||||
var number uint64
|
|
||||||
for number = frozen; number <= head; number++ {
|
|
||||||
if present, _ := db.Has(headerHashKey(number)); present {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We are about to exit on error. Print database metadata before exiting
|
|
||||||
printChainMetadata(db)
|
|
||||||
return nil, fmt.Errorf("gap in the chain between ancients [0 - #%d] and leveldb [#%d - #%d] ",
|
|
||||||
frozen-1, number, head)
|
|
||||||
}
|
|
||||||
// Database contains only older data than the freezer, this happens if the
|
|
||||||
// state was wiped and reinited from an existing freezer.
|
|
||||||
}
|
|
||||||
// Otherwise, key-value store continues where the freezer left off, all is fine.
|
|
||||||
// We might have duplicate blocks (crash after freezer write but before key-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 {
|
|
||||||
printChainMetadata(db)
|
|
||||||
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 freezer
|
|
||||||
}
|
|
||||||
// Otherwise, the head header is still the genesis, we're allowed to init a new
|
|
||||||
// freezer.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Freezer is consistent with the key-value database, permit combining the two
|
|
||||||
if !frdb.readonly {
|
|
||||||
frdb.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
frdb.freeze(db)
|
|
||||||
frdb.wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return &freezerdb{
|
|
||||||
ancientRoot: ancient,
|
|
||||||
KeyValueStore: db,
|
|
||||||
AncientStore: frdb,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMemoryDatabase creates an ephemeral in-memory key-value database without a
|
|
||||||
// freezer moving immutable chain segments into cold storage.
|
|
||||||
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.
|
|
||||||
func NewMemoryDatabaseWithCap(size int) ethdb.Database {
|
|
||||||
return NewDatabase(memorydb.NewWithCap(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLevelDBDatabase creates a persistent key-value database without a freezer
|
|
||||||
// moving immutable chain segments into cold storage.
|
|
||||||
func NewLevelDBDatabase(file string, cache int, handles int, namespace string, readonly bool) (ethdb.Database, error) {
|
|
||||||
db, err := leveldb.New(file, cache, handles, namespace, readonly)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Info("Using LevelDB as the backing database")
|
|
||||||
return NewDatabase(db), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPebbleDBDatabase creates a persistent key-value database without a freezer
|
|
||||||
// moving immutable chain segments into cold storage.
|
|
||||||
func NewPebbleDBDatabase(file string, cache int, handles int, namespace string, readonly, ephemeral bool) (ethdb.Database, error) {
|
|
||||||
db, err := pebble.New(file, cache, handles, namespace, readonly, ephemeral)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewDatabase(db), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
dbPebble = "pebble"
|
|
||||||
dbLeveldb = "leveldb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PreexistingDatabase checks the given data directory whether a database is already
|
|
||||||
// instantiated at that location, and if so, returns the type of database (or the
|
|
||||||
// empty string).
|
|
||||||
func PreexistingDatabase(path string) string {
|
|
||||||
if _, err := os.Stat(filepath.Join(path, "CURRENT")); err != nil {
|
|
||||||
return "" // No pre-existing db
|
|
||||||
}
|
|
||||||
if matches, err := filepath.Glob(filepath.Join(path, "OPTIONS*")); len(matches) > 0 || err != nil {
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // only possible if the pattern is malformed
|
|
||||||
}
|
|
||||||
return dbPebble
|
|
||||||
}
|
|
||||||
return dbLeveldb
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenOptions contains the options to apply when opening a database.
|
|
||||||
// OBS: If AncientsDirectory is empty, it indicates that no freezer is to be used.
|
|
||||||
type OpenOptions struct {
|
|
||||||
Type string // "leveldb" | "pebble"
|
|
||||||
Directory string // the datadir
|
|
||||||
AncientsDirectory string // the ancients-dir
|
|
||||||
Namespace string // the namespace for database relevant metrics
|
|
||||||
Cache int // the capacity(in megabytes) of the data caching
|
|
||||||
Handles int // number of files to be open simultaneously
|
|
||||||
ReadOnly bool
|
|
||||||
// Ephemeral means that filesystem sync operations should be avoided: data integrity in the face of
|
|
||||||
// a crash is not important. This option should typically be used in tests.
|
|
||||||
Ephemeral bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// openKeyValueDatabase opens a disk-based key-value database, e.g. leveldb or pebble.
|
|
||||||
//
|
|
||||||
// type == null type != null
|
|
||||||
// +----------------------------------------
|
|
||||||
// db is non-existent | pebble default | specified type
|
|
||||||
// db is existent | from db | specified type (if compatible)
|
|
||||||
func openKeyValueDatabase(o OpenOptions) (ethdb.Database, error) {
|
|
||||||
// Reject any unsupported database type
|
|
||||||
if len(o.Type) != 0 && o.Type != dbLeveldb && o.Type != dbPebble {
|
|
||||||
return nil, fmt.Errorf("unknown db.engine %v", o.Type)
|
|
||||||
}
|
|
||||||
// Retrieve any pre-existing database's type and use that or the requested one
|
|
||||||
// as long as there's no conflict between the two types
|
|
||||||
existingDb := PreexistingDatabase(o.Directory)
|
|
||||||
if len(existingDb) != 0 && len(o.Type) != 0 && o.Type != existingDb {
|
|
||||||
return nil, fmt.Errorf("db.engine choice was %v but found pre-existing %v database in specified data directory", o.Type, existingDb)
|
|
||||||
}
|
|
||||||
if o.Type == dbPebble || existingDb == dbPebble {
|
|
||||||
log.Info("Using pebble as the backing database")
|
|
||||||
return NewPebbleDBDatabase(o.Directory, o.Cache, o.Handles, o.Namespace, o.ReadOnly, o.Ephemeral)
|
|
||||||
}
|
|
||||||
if o.Type == dbLeveldb || existingDb == dbLeveldb {
|
|
||||||
log.Info("Using leveldb as the backing database")
|
|
||||||
return NewLevelDBDatabase(o.Directory, o.Cache, o.Handles, o.Namespace, o.ReadOnly)
|
|
||||||
}
|
|
||||||
// No pre-existing database, no user-requested one either. Default to Pebble.
|
|
||||||
log.Info("Defaulting to pebble as the backing database")
|
|
||||||
return NewPebbleDBDatabase(o.Directory, o.Cache, o.Handles, o.Namespace, o.ReadOnly, o.Ephemeral)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open opens both a disk-based key-value database such as leveldb or pebble, but also
|
|
||||||
// integrates it with a freezer database -- if the AncientDir option has been
|
|
||||||
// set on the provided OpenOptions.
|
|
||||||
// The passed o.AncientDir indicates the path of root ancient directory where
|
|
||||||
// the chain freezer can be opened.
|
|
||||||
func Open(o OpenOptions) (ethdb.Database, error) {
|
|
||||||
kvdb, err := openKeyValueDatabase(o)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(o.AncientsDirectory) == 0 {
|
|
||||||
return kvdb, nil
|
|
||||||
}
|
|
||||||
frdb, err := NewDatabaseWithFreezer(kvdb, o.AncientsDirectory, o.Namespace, o.ReadOnly)
|
|
||||||
if err != nil {
|
|
||||||
kvdb.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return frdb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type counter uint64
|
|
||||||
|
|
||||||
func (c counter) String() string {
|
|
||||||
return fmt.Sprintf("%d", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c counter) Percentage(current uint64) string {
|
|
||||||
return fmt.Sprintf("%d", current*100/uint64(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
// stat stores sizes and count for a parameter
|
|
||||||
type stat struct {
|
|
||||||
size common.StorageSize
|
|
||||||
count counter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add size to the stat and increase the counter by 1
|
|
||||||
func (s *stat) Add(size common.StorageSize) {
|
|
||||||
s.size += size
|
|
||||||
s.count++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stat) Size() string {
|
|
||||||
return s.size.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stat) Count() string {
|
|
||||||
return s.count.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// InspectDatabase traverses the entire database and checks the size
|
|
||||||
// of all different categories of data.
|
|
||||||
func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
|
|
||||||
it := db.NewIterator(keyPrefix, keyStart)
|
|
||||||
defer it.Release()
|
|
||||||
|
|
||||||
var (
|
|
||||||
count int64
|
|
||||||
start = time.Now()
|
|
||||||
logged = time.Now()
|
|
||||||
|
|
||||||
// Key-value store statistics
|
|
||||||
headers stat
|
|
||||||
bodies stat
|
|
||||||
receipts stat
|
|
||||||
tds stat
|
|
||||||
numHashPairings stat
|
|
||||||
hashNumPairings stat
|
|
||||||
legacyTries stat
|
|
||||||
stateLookups stat
|
|
||||||
accountTries stat
|
|
||||||
storageTries stat
|
|
||||||
codes stat
|
|
||||||
txLookups stat
|
|
||||||
accountSnaps stat
|
|
||||||
storageSnaps stat
|
|
||||||
preimages stat
|
|
||||||
bloomBits stat
|
|
||||||
beaconHeaders stat
|
|
||||||
cliqueSnaps stat
|
|
||||||
|
|
||||||
// Les statistic
|
|
||||||
chtTrieNodes stat
|
|
||||||
bloomTrieNodes stat
|
|
||||||
|
|
||||||
// Meta- and unaccounted data
|
|
||||||
metadata stat
|
|
||||||
unaccounted stat
|
|
||||||
|
|
||||||
// Totals
|
|
||||||
total 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) && len(key) == (len(headerPrefix)+8+common.HashLength):
|
|
||||||
headers.Add(size)
|
|
||||||
case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength):
|
|
||||||
bodies.Add(size)
|
|
||||||
case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength):
|
|
||||||
receipts.Add(size)
|
|
||||||
case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix):
|
|
||||||
tds.Add(size)
|
|
||||||
case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix):
|
|
||||||
numHashPairings.Add(size)
|
|
||||||
case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
|
|
||||||
hashNumPairings.Add(size)
|
|
||||||
case IsLegacyTrieNode(key, it.Value()):
|
|
||||||
legacyTries.Add(size)
|
|
||||||
case bytes.HasPrefix(key, stateIDPrefix) && len(key) == len(stateIDPrefix)+common.HashLength:
|
|
||||||
stateLookups.Add(size)
|
|
||||||
case IsAccountTrieNode(key):
|
|
||||||
accountTries.Add(size)
|
|
||||||
case IsStorageTrieNode(key):
|
|
||||||
storageTries.Add(size)
|
|
||||||
case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength:
|
|
||||||
codes.Add(size)
|
|
||||||
case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength):
|
|
||||||
txLookups.Add(size)
|
|
||||||
case bytes.HasPrefix(key, SnapshotAccountPrefix) && len(key) == (len(SnapshotAccountPrefix)+common.HashLength):
|
|
||||||
accountSnaps.Add(size)
|
|
||||||
case bytes.HasPrefix(key, SnapshotStoragePrefix) && len(key) == (len(SnapshotStoragePrefix)+2*common.HashLength):
|
|
||||||
storageSnaps.Add(size)
|
|
||||||
case bytes.HasPrefix(key, PreimagePrefix) && len(key) == (len(PreimagePrefix)+common.HashLength):
|
|
||||||
preimages.Add(size)
|
|
||||||
case bytes.HasPrefix(key, configPrefix) && len(key) == (len(configPrefix)+common.HashLength):
|
|
||||||
metadata.Add(size)
|
|
||||||
case bytes.HasPrefix(key, genesisPrefix) && len(key) == (len(genesisPrefix)+common.HashLength):
|
|
||||||
metadata.Add(size)
|
|
||||||
case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength):
|
|
||||||
bloomBits.Add(size)
|
|
||||||
case bytes.HasPrefix(key, BloomBitsIndexPrefix):
|
|
||||||
bloomBits.Add(size)
|
|
||||||
case bytes.HasPrefix(key, skeletonHeaderPrefix) && len(key) == (len(skeletonHeaderPrefix)+8):
|
|
||||||
beaconHeaders.Add(size)
|
|
||||||
case bytes.HasPrefix(key, CliqueSnapshotPrefix) && len(key) == 7+common.HashLength:
|
|
||||||
cliqueSnaps.Add(size)
|
|
||||||
case bytes.HasPrefix(key, ChtTablePrefix) ||
|
|
||||||
bytes.HasPrefix(key, ChtIndexTablePrefix) ||
|
|
||||||
bytes.HasPrefix(key, ChtPrefix): // Canonical hash trie
|
|
||||||
chtTrieNodes.Add(size)
|
|
||||||
case bytes.HasPrefix(key, BloomTrieTablePrefix) ||
|
|
||||||
bytes.HasPrefix(key, BloomTrieIndexPrefix) ||
|
|
||||||
bytes.HasPrefix(key, BloomTriePrefix): // Bloomtrie sub
|
|
||||||
bloomTrieNodes.Add(size)
|
|
||||||
default:
|
|
||||||
var accounted bool
|
|
||||||
for _, meta := range [][]byte{
|
|
||||||
databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, headFinalizedBlockKey,
|
|
||||||
lastPivotKey, fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotJournalKey,
|
|
||||||
snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey,
|
|
||||||
uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey,
|
|
||||||
persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, snapSyncStatusFlagKey,
|
|
||||||
} {
|
|
||||||
if bytes.Equal(key, meta) {
|
|
||||||
metadata.Add(size)
|
|
||||||
accounted = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !accounted {
|
|
||||||
unaccounted.Add(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Display the database statistic of key-value store.
|
|
||||||
stats := [][]string{
|
|
||||||
{"Key-Value store", "Headers", headers.Size(), headers.Count()},
|
|
||||||
{"Key-Value store", "Bodies", bodies.Size(), bodies.Count()},
|
|
||||||
{"Key-Value store", "Receipt lists", receipts.Size(), receipts.Count()},
|
|
||||||
{"Key-Value store", "Difficulties", tds.Size(), tds.Count()},
|
|
||||||
{"Key-Value store", "Block number->hash", numHashPairings.Size(), numHashPairings.Count()},
|
|
||||||
{"Key-Value store", "Block hash->number", hashNumPairings.Size(), hashNumPairings.Count()},
|
|
||||||
{"Key-Value store", "Transaction index", txLookups.Size(), txLookups.Count()},
|
|
||||||
{"Key-Value store", "Bloombit index", bloomBits.Size(), bloomBits.Count()},
|
|
||||||
{"Key-Value store", "Contract codes", codes.Size(), codes.Count()},
|
|
||||||
{"Key-Value store", "Hash trie nodes", legacyTries.Size(), legacyTries.Count()},
|
|
||||||
{"Key-Value store", "Path trie state lookups", stateLookups.Size(), stateLookups.Count()},
|
|
||||||
{"Key-Value store", "Path trie account nodes", accountTries.Size(), accountTries.Count()},
|
|
||||||
{"Key-Value store", "Path trie storage nodes", storageTries.Size(), storageTries.Count()},
|
|
||||||
{"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()},
|
|
||||||
{"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()},
|
|
||||||
{"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()},
|
|
||||||
{"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()},
|
|
||||||
{"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()},
|
|
||||||
{"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()},
|
|
||||||
{"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()},
|
|
||||||
{"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()},
|
|
||||||
}
|
|
||||||
// Inspect all registered append-only file store then.
|
|
||||||
ancients, err := inspectFreezers(db)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, ancient := range ancients {
|
|
||||||
for _, table := range ancient.sizes {
|
|
||||||
stats = append(stats, []string{
|
|
||||||
fmt.Sprintf("Ancient store (%s)", strings.Title(ancient.name)),
|
|
||||||
strings.Title(table.name),
|
|
||||||
table.size.String(),
|
|
||||||
fmt.Sprintf("%d", ancient.count()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
total += ancient.size()
|
|
||||||
}
|
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
|
||||||
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
|
|
||||||
table.SetFooter([]string{"", "Total", total.String(), " "})
|
|
||||||
table.AppendBulk(stats)
|
|
||||||
table.Render()
|
|
||||||
|
|
||||||
if unaccounted.size > 0 {
|
|
||||||
log.Error("Database contains unaccounted data", "size", unaccounted.size, "count", unaccounted.count)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// printChainMetadata prints out chain metadata to stderr.
|
|
||||||
func printChainMetadata(db ethdb.KeyValueStore) {
|
|
||||||
fmt.Fprintf(os.Stderr, "Chain metadata\n")
|
|
||||||
for _, v := range ReadChainMetadata(db) {
|
|
||||||
fmt.Fprintf(os.Stderr, " %s\n", strings.Join(v, ": "))
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadChainMetadata returns a set of key/value pairs that contains information
|
|
||||||
// about the database chain status. This can be used for diagnostic purposes
|
|
||||||
// when investigating the state of the node.
|
|
||||||
func ReadChainMetadata(db ethdb.KeyValueStore) [][]string {
|
|
||||||
pp := func(val *uint64) string {
|
|
||||||
if val == nil {
|
|
||||||
return "<nil>"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d (%#x)", *val, *val)
|
|
||||||
}
|
|
||||||
data := [][]string{
|
|
||||||
{"databaseVersion", pp(ReadDatabaseVersion(db))},
|
|
||||||
{"headBlockHash", fmt.Sprintf("%v", ReadHeadBlockHash(db))},
|
|
||||||
{"headFastBlockHash", fmt.Sprintf("%v", ReadHeadFastBlockHash(db))},
|
|
||||||
{"headHeaderHash", fmt.Sprintf("%v", ReadHeadHeaderHash(db))},
|
|
||||||
{"lastPivotNumber", pp(ReadLastPivotNumber(db))},
|
|
||||||
{"len(snapshotSyncStatus)", fmt.Sprintf("%d bytes", len(ReadSnapshotSyncStatus(db)))},
|
|
||||||
{"snapshotDisabled", fmt.Sprintf("%v", ReadSnapshotDisabled(db))},
|
|
||||||
{"snapshotJournal", fmt.Sprintf("%d bytes", len(ReadSnapshotJournal(db)))},
|
|
||||||
{"snapshotRecoveryNumber", pp(ReadSnapshotRecoveryNumber(db))},
|
|
||||||
{"snapshotRoot", fmt.Sprintf("%v", ReadSnapshotRoot(db))},
|
|
||||||
{"txIndexTail", pp(ReadTxIndexTail(db))},
|
|
||||||
{"fastTxLookupLimit", pp(ReadFastTxLookupLimit(db))},
|
|
||||||
}
|
|
||||||
if b := ReadSkeletonSyncStatus(db); b != nil {
|
|
||||||
data = append(data, []string{"SkeletonSyncStatus", string(b)})
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
@ -1,509 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"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/gofrs/flock"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// errReadOnly is returned if the freezer is opened in read only mode. All the
|
|
||||||
// mutations are disallowed.
|
|
||||||
errReadOnly = errors.New("read only")
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
|
|
||||||
// errSymlinkDatadir is returned if the ancient directory specified by user
|
|
||||||
// is a symbolic link.
|
|
||||||
errSymlinkDatadir = errors.New("symbolic link datadir is not supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
// freezerTableSize defines the maximum size of freezer data files.
|
|
||||||
const freezerTableSize = 2 * 1000 * 1000 * 1000
|
|
||||||
|
|
||||||
// Freezer is a memory mapped append-only database to store immutable ordered
|
|
||||||
// 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 {
|
|
||||||
frozen atomic.Uint64 // Number of blocks already frozen
|
|
||||||
tail atomic.Uint64 // Number of the first stored item in the freezer
|
|
||||||
|
|
||||||
// This lock synchronizes writers and the truncate operation, as well as
|
|
||||||
// the "atomic" (batched) read operations.
|
|
||||||
writeLock sync.RWMutex
|
|
||||||
writeBatch *freezerBatch
|
|
||||||
|
|
||||||
readonly bool
|
|
||||||
tables map[string]*freezerTable // Data tables for storing everything
|
|
||||||
instanceLock *flock.Flock // File-system lock to prevent double opens
|
|
||||||
closeOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChainFreezer is a small utility method around NewFreezer that sets the
|
|
||||||
// default parameters for the chain storage.
|
|
||||||
func NewChainFreezer(datadir string, namespace string, readonly bool) (*Freezer, error) {
|
|
||||||
return NewFreezer(datadir, namespace, readonly, freezerTableSize, chainFreezerNoSnappy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFreezer creates a freezer instance for maintaining immutable ordered
|
|
||||||
// data according to the given parameters.
|
|
||||||
//
|
|
||||||
// The 'tables' argument defines the data tables. If the value of a map
|
|
||||||
// entry is true, snappy compression is disabled for the table.
|
|
||||||
func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*Freezer, error) {
|
|
||||||
// Create the initial freezer object
|
|
||||||
var (
|
|
||||||
readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil)
|
|
||||||
writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil)
|
|
||||||
sizeGauge = metrics.NewRegisteredGauge(namespace+"ancient/size", 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flockFile := filepath.Join(datadir, "FLOCK")
|
|
||||||
if err := os.MkdirAll(filepath.Dir(flockFile), 0755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Leveldb uses LOCK as the filelock filename. To prevent the
|
|
||||||
// name collision, we use FLOCK as the lock name.
|
|
||||||
lock := flock.New(flockFile)
|
|
||||||
tryLock := lock.TryLock
|
|
||||||
if readonly {
|
|
||||||
tryLock = lock.TryRLock
|
|
||||||
}
|
|
||||||
if locked, err := tryLock(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if !locked {
|
|
||||||
return nil, errors.New("locking failed")
|
|
||||||
}
|
|
||||||
// Open all the supported data tables
|
|
||||||
freezer := &Freezer{
|
|
||||||
readonly: readonly,
|
|
||||||
tables: make(map[string]*freezerTable),
|
|
||||||
instanceLock: lock,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the tables.
|
|
||||||
for name, disableSnappy := range tables {
|
|
||||||
table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, maxTableSize, disableSnappy, readonly)
|
|
||||||
if err != nil {
|
|
||||||
for _, table := range freezer.tables {
|
|
||||||
table.Close()
|
|
||||||
}
|
|
||||||
lock.Unlock()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
freezer.tables[name] = table
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
if freezer.readonly {
|
|
||||||
// In readonly mode only validate, don't truncate.
|
|
||||||
// validate also sets `freezer.frozen`.
|
|
||||||
err = freezer.validate()
|
|
||||||
} else {
|
|
||||||
// Truncate all tables to common length.
|
|
||||||
err = freezer.repair()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
for _, table := range freezer.tables {
|
|
||||||
table.Close()
|
|
||||||
}
|
|
||||||
lock.Unlock()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the write batch.
|
|
||||||
freezer.writeBatch = newFreezerBatch(freezer)
|
|
||||||
|
|
||||||
log.Info("Opened ancient database", "database", datadir, "readonly", readonly)
|
|
||||||
return freezer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close terminates the chain freezer, unmapping all the data files.
|
|
||||||
func (f *Freezer) Close() error {
|
|
||||||
f.writeLock.Lock()
|
|
||||||
defer f.writeLock.Unlock()
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
f.closeOnce.Do(func() {
|
|
||||||
for _, table := range f.tables {
|
|
||||||
if err := table.Close(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := f.instanceLock.Unlock(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if errs != nil {
|
|
||||||
return fmt.Errorf("%v", errs)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientRange retrieves multiple items in sequence, starting from the index 'start'.
|
|
||||||
// It will return
|
|
||||||
// - at most 'count' items,
|
|
||||||
// - if maxBytes is specified: at least 1 item (even if exceeding the maxByteSize),
|
|
||||||
// but will otherwise return as many items as fit into maxByteSize.
|
|
||||||
// - if maxBytes is not specified, 'count' items will be returned if they are present.
|
|
||||||
func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
|
|
||||||
if table := f.tables[kind]; table != nil {
|
|
||||||
return table.RetrieveItems(start, count, maxBytes)
|
|
||||||
}
|
|
||||||
return nil, errUnknownTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancients returns the length of the frozen items.
|
|
||||||
func (f *Freezer) Ancients() (uint64, error) {
|
|
||||||
return f.frozen.Load(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tail returns the number of first stored item in the freezer.
|
|
||||||
func (f *Freezer) Tail() (uint64, error) {
|
|
||||||
return f.tail.Load(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientSize returns the ancient size of the specified category.
|
|
||||||
func (f *Freezer) AncientSize(kind string) (uint64, error) {
|
|
||||||
// This needs the write lock to avoid data races on table fields.
|
|
||||||
// Speed doesn't matter here, AncientSize is for debugging.
|
|
||||||
f.writeLock.RLock()
|
|
||||||
defer f.writeLock.RUnlock()
|
|
||||||
|
|
||||||
if table := f.tables[kind]; table != nil {
|
|
||||||
return table.size()
|
|
||||||
}
|
|
||||||
return 0, errUnknownTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAncients runs the given read operation while ensuring that no writes take place
|
|
||||||
// on the underlying freezer.
|
|
||||||
func (f *Freezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
|
|
||||||
f.writeLock.RLock()
|
|
||||||
defer f.writeLock.RUnlock()
|
|
||||||
|
|
||||||
return fn(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyAncients runs the given write operation.
|
|
||||||
func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
|
|
||||||
if f.readonly {
|
|
||||||
return 0, errReadOnly
|
|
||||||
}
|
|
||||||
f.writeLock.Lock()
|
|
||||||
defer f.writeLock.Unlock()
|
|
||||||
|
|
||||||
// Roll back all tables to the starting position in case of error.
|
|
||||||
prevItem := f.frozen.Load()
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
// The write operation has failed. Go back to the previous item position.
|
|
||||||
for name, table := range f.tables {
|
|
||||||
err := table.truncateHead(prevItem)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Freezer table roll-back failed", "table", name, "index", prevItem, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
f.writeBatch.reset()
|
|
||||||
if err := fn(f.writeBatch); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
item, writeSize, err := f.writeBatch.commit()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
f.frozen.Store(item)
|
|
||||||
return writeSize, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateHead discards any recent data above the provided threshold number.
|
|
||||||
// It returns the previous head number.
|
|
||||||
func (f *Freezer) TruncateHead(items uint64) (uint64, error) {
|
|
||||||
if f.readonly {
|
|
||||||
return 0, errReadOnly
|
|
||||||
}
|
|
||||||
f.writeLock.Lock()
|
|
||||||
defer f.writeLock.Unlock()
|
|
||||||
|
|
||||||
oitems := f.frozen.Load()
|
|
||||||
if oitems <= items {
|
|
||||||
return oitems, nil
|
|
||||||
}
|
|
||||||
for _, table := range f.tables {
|
|
||||||
if err := table.truncateHead(items); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.frozen.Store(items)
|
|
||||||
return oitems, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateTail discards any recent data below the provided threshold number.
|
|
||||||
func (f *Freezer) TruncateTail(tail uint64) (uint64, error) {
|
|
||||||
if f.readonly {
|
|
||||||
return 0, errReadOnly
|
|
||||||
}
|
|
||||||
f.writeLock.Lock()
|
|
||||||
defer f.writeLock.Unlock()
|
|
||||||
|
|
||||||
old := f.tail.Load()
|
|
||||||
if old >= tail {
|
|
||||||
return old, nil
|
|
||||||
}
|
|
||||||
for _, table := range f.tables {
|
|
||||||
if err := table.truncateTail(tail); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.tail.Store(tail)
|
|
||||||
return old, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate checks that every table has the same boundary.
|
|
||||||
// Used instead of `repair` in readonly mode.
|
|
||||||
func (f *Freezer) validate() error {
|
|
||||||
if len(f.tables) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
head uint64
|
|
||||||
tail uint64
|
|
||||||
name string
|
|
||||||
)
|
|
||||||
// Hack to get boundary of any table
|
|
||||||
for kind, table := range f.tables {
|
|
||||||
head = table.items.Load()
|
|
||||||
tail = table.itemHidden.Load()
|
|
||||||
name = kind
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Now check every table against those boundaries.
|
|
||||||
for kind, table := range f.tables {
|
|
||||||
if head != table.items.Load() {
|
|
||||||
return fmt.Errorf("freezer tables %s and %s have differing head: %d != %d", kind, name, table.items.Load(), head)
|
|
||||||
}
|
|
||||||
if tail != table.itemHidden.Load() {
|
|
||||||
return fmt.Errorf("freezer tables %s and %s have differing tail: %d != %d", kind, name, table.itemHidden.Load(), tail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.frozen.Store(head)
|
|
||||||
f.tail.Store(tail)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// repair truncates all data tables to the same length.
|
|
||||||
func (f *Freezer) repair() error {
|
|
||||||
var (
|
|
||||||
head = uint64(math.MaxUint64)
|
|
||||||
tail = uint64(0)
|
|
||||||
)
|
|
||||||
for _, table := range f.tables {
|
|
||||||
items := table.items.Load()
|
|
||||||
if head > items {
|
|
||||||
head = items
|
|
||||||
}
|
|
||||||
hidden := table.itemHidden.Load()
|
|
||||||
if hidden > tail {
|
|
||||||
tail = hidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, table := range f.tables {
|
|
||||||
if err := table.truncateHead(head); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := table.truncateTail(tail); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.frozen.Store(head)
|
|
||||||
f.tail.Store(tail)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertLegacyFn takes a raw freezer entry in an older format and
|
|
||||||
// returns it in the new format.
|
|
||||||
type convertLegacyFn = func([]byte) ([]byte, error)
|
|
||||||
|
|
||||||
// MigrateTable processes the entries in a given table in sequence
|
|
||||||
// converting them to a new format if they're of an old format.
|
|
||||||
func (f *Freezer) MigrateTable(kind string, convert convertLegacyFn) error {
|
|
||||||
if f.readonly {
|
|
||||||
return errReadOnly
|
|
||||||
}
|
|
||||||
f.writeLock.Lock()
|
|
||||||
defer f.writeLock.Unlock()
|
|
||||||
|
|
||||||
table, ok := f.tables[kind]
|
|
||||||
if !ok {
|
|
||||||
return errUnknownTable
|
|
||||||
}
|
|
||||||
// forEach iterates every entry in the table serially and in order, calling `fn`
|
|
||||||
// with the item as argument. If `fn` returns an error the iteration stops
|
|
||||||
// and that error will be returned.
|
|
||||||
forEach := func(t *freezerTable, offset uint64, fn func(uint64, []byte) error) error {
|
|
||||||
var (
|
|
||||||
items = t.items.Load()
|
|
||||||
batchSize = uint64(1024)
|
|
||||||
maxBytes = uint64(1024 * 1024)
|
|
||||||
)
|
|
||||||
for i := offset; i < items; {
|
|
||||||
if i+batchSize > items {
|
|
||||||
batchSize = items - i
|
|
||||||
}
|
|
||||||
data, err := t.RetrieveItems(i, batchSize, maxBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for j, item := range data {
|
|
||||||
if err := fn(i+uint64(j), item); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += uint64(len(data))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// TODO(s1na): This is a sanity-check since as of now no process does tail-deletion. But the migration
|
|
||||||
// process assumes no deletion at tail and needs to be modified to account for that.
|
|
||||||
if table.itemOffset.Load() > 0 || table.itemHidden.Load() > 0 {
|
|
||||||
return errors.New("migration not supported for tail-deleted freezers")
|
|
||||||
}
|
|
||||||
ancientsPath := filepath.Dir(table.index.Name())
|
|
||||||
// Set up new dir for the migrated table, the content of which
|
|
||||||
// we'll at the end move over to the ancients dir.
|
|
||||||
migrationPath := filepath.Join(ancientsPath, "migration")
|
|
||||||
newTable, err := newFreezerTable(migrationPath, kind, table.noCompression, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
batch = newTable.newBatch()
|
|
||||||
out []byte
|
|
||||||
start = time.Now()
|
|
||||||
logged = time.Now()
|
|
||||||
offset = newTable.items.Load()
|
|
||||||
)
|
|
||||||
if offset > 0 {
|
|
||||||
log.Info("found previous migration attempt", "migrated", offset)
|
|
||||||
}
|
|
||||||
// Iterate through entries and transform them
|
|
||||||
if err := forEach(table, offset, func(i uint64, blob []byte) error {
|
|
||||||
if i%10000 == 0 && time.Since(logged) > 16*time.Second {
|
|
||||||
log.Info("Processing legacy elements", "count", i, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
out, err = convert(blob)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := batch.AppendRaw(i, out); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := batch.commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("Replacing old table files with migrated ones", "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
// Release and delete old table files. Note this won't
|
|
||||||
// delete the index file.
|
|
||||||
table.releaseFilesAfter(0, true)
|
|
||||||
|
|
||||||
if err := newTable.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
files, err := os.ReadDir(migrationPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Move migrated files to ancients dir.
|
|
||||||
for _, f := range files {
|
|
||||||
// This will replace the old index file as a side-effect.
|
|
||||||
if err := os.Rename(filepath.Join(migrationPath, f.Name()), filepath.Join(ancientsPath, f.Name())); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete by now empty dir.
|
|
||||||
if err := os.Remove(migrationPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/golang/snappy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is the maximum amount of data that will be buffered in memory
|
|
||||||
// for a single freezer table batch.
|
|
||||||
const freezerBatchBufferLimit = 2 * 1024 * 1024
|
|
||||||
|
|
||||||
// freezerBatch is a write operation of multiple items on a freezer.
|
|
||||||
type freezerBatch struct {
|
|
||||||
tables map[string]*freezerTableBatch
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFreezerBatch(f *Freezer) *freezerBatch {
|
|
||||||
batch := &freezerBatch{tables: make(map[string]*freezerTableBatch, len(f.tables))}
|
|
||||||
for kind, table := range f.tables {
|
|
||||||
batch.tables[kind] = table.newBatch()
|
|
||||||
}
|
|
||||||
return batch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append adds an RLP-encoded item of the given kind.
|
|
||||||
func (batch *freezerBatch) Append(kind string, num uint64, item interface{}) error {
|
|
||||||
return batch.tables[kind].Append(num, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendRaw adds an item of the given kind.
|
|
||||||
func (batch *freezerBatch) AppendRaw(kind string, num uint64, item []byte) error {
|
|
||||||
return batch.tables[kind].AppendRaw(num, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset initializes the batch.
|
|
||||||
func (batch *freezerBatch) reset() {
|
|
||||||
for _, tb := range batch.tables {
|
|
||||||
tb.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit is called at the end of a write operation and
|
|
||||||
// writes all remaining data to tables.
|
|
||||||
func (batch *freezerBatch) commit() (item uint64, writeSize int64, err error) {
|
|
||||||
// Check that count agrees on all batches.
|
|
||||||
item = uint64(math.MaxUint64)
|
|
||||||
for name, tb := range batch.tables {
|
|
||||||
if item < math.MaxUint64 && tb.curItem != item {
|
|
||||||
return 0, 0, fmt.Errorf("table %s is at item %d, want %d", name, tb.curItem, item)
|
|
||||||
}
|
|
||||||
item = tb.curItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit all table batches.
|
|
||||||
for _, tb := range batch.tables {
|
|
||||||
if err := tb.commit(); err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
writeSize += tb.totalBytes
|
|
||||||
}
|
|
||||||
return item, writeSize, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// freezerTableBatch is a batch for a freezer table.
|
|
||||||
type freezerTableBatch struct {
|
|
||||||
t *freezerTable
|
|
||||||
|
|
||||||
sb *snappyBuffer
|
|
||||||
encBuffer writeBuffer
|
|
||||||
dataBuffer []byte
|
|
||||||
indexBuffer []byte
|
|
||||||
curItem uint64 // expected index of next append
|
|
||||||
totalBytes int64 // counts written bytes since reset
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBatch creates a new batch for the freezer table.
|
|
||||||
func (t *freezerTable) newBatch() *freezerTableBatch {
|
|
||||||
batch := &freezerTableBatch{t: t}
|
|
||||||
if !t.noCompression {
|
|
||||||
batch.sb = new(snappyBuffer)
|
|
||||||
}
|
|
||||||
batch.reset()
|
|
||||||
return batch
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset clears the batch for reuse.
|
|
||||||
func (batch *freezerTableBatch) reset() {
|
|
||||||
batch.dataBuffer = batch.dataBuffer[:0]
|
|
||||||
batch.indexBuffer = batch.indexBuffer[:0]
|
|
||||||
batch.curItem = batch.t.items.Load()
|
|
||||||
batch.totalBytes = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append rlp-encodes and adds data 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.
|
|
||||||
func (batch *freezerTableBatch) Append(item uint64, data interface{}) error {
|
|
||||||
if item != batch.curItem {
|
|
||||||
return fmt.Errorf("%w: have %d want %d", errOutOrderInsertion, item, batch.curItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode the item.
|
|
||||||
batch.encBuffer.Reset()
|
|
||||||
if err := rlp.Encode(&batch.encBuffer, data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
encItem := batch.encBuffer.data
|
|
||||||
if batch.sb != nil {
|
|
||||||
encItem = batch.sb.compress(encItem)
|
|
||||||
}
|
|
||||||
return batch.appendItem(encItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendRaw 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.
|
|
||||||
func (batch *freezerTableBatch) AppendRaw(item uint64, blob []byte) error {
|
|
||||||
if item != batch.curItem {
|
|
||||||
return fmt.Errorf("%w: have %d want %d", errOutOrderInsertion, item, batch.curItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
encItem := blob
|
|
||||||
if batch.sb != nil {
|
|
||||||
encItem = batch.sb.compress(blob)
|
|
||||||
}
|
|
||||||
return batch.appendItem(encItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (batch *freezerTableBatch) appendItem(data []byte) error {
|
|
||||||
// Check if item fits into current data file.
|
|
||||||
itemSize := int64(len(data))
|
|
||||||
itemOffset := batch.t.headBytes + int64(len(batch.dataBuffer))
|
|
||||||
if itemOffset+itemSize > int64(batch.t.maxFileSize) {
|
|
||||||
// It doesn't fit, go to next file first.
|
|
||||||
if err := batch.commit(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := batch.t.advanceHead(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
itemOffset = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put data to buffer.
|
|
||||||
batch.dataBuffer = append(batch.dataBuffer, data...)
|
|
||||||
batch.totalBytes += itemSize
|
|
||||||
|
|
||||||
// Put index entry to buffer.
|
|
||||||
entry := indexEntry{filenum: batch.t.headId, offset: uint32(itemOffset + itemSize)}
|
|
||||||
batch.indexBuffer = entry.append(batch.indexBuffer)
|
|
||||||
batch.curItem++
|
|
||||||
|
|
||||||
return batch.maybeCommit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// maybeCommit writes the buffered data if the buffer is full enough.
|
|
||||||
func (batch *freezerTableBatch) maybeCommit() error {
|
|
||||||
if len(batch.dataBuffer) > freezerBatchBufferLimit {
|
|
||||||
return batch.commit()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit writes the batched items to the backing freezerTable.
|
|
||||||
func (batch *freezerTableBatch) commit() error {
|
|
||||||
// Write data. The head file is fsync'd after write to ensure the
|
|
||||||
// data is truly transferred to disk.
|
|
||||||
_, err := batch.t.head.Write(batch.dataBuffer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := batch.t.head.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dataSize := int64(len(batch.dataBuffer))
|
|
||||||
batch.dataBuffer = batch.dataBuffer[:0]
|
|
||||||
|
|
||||||
// Write indices. The index file is fsync'd after write to ensure the
|
|
||||||
// data indexes are truly transferred to disk.
|
|
||||||
_, err = batch.t.index.Write(batch.indexBuffer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := batch.t.index.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
indexSize := int64(len(batch.indexBuffer))
|
|
||||||
batch.indexBuffer = batch.indexBuffer[:0]
|
|
||||||
|
|
||||||
// Update headBytes of table.
|
|
||||||
batch.t.headBytes += dataSize
|
|
||||||
batch.t.items.Store(batch.curItem)
|
|
||||||
|
|
||||||
// Update metrics.
|
|
||||||
batch.t.sizeGauge.Inc(dataSize + indexSize)
|
|
||||||
batch.t.writeMeter.Mark(dataSize + indexSize)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// snappyBuffer writes snappy in block format, and can be reused. It is
|
|
||||||
// reset when WriteTo is called.
|
|
||||||
type snappyBuffer struct {
|
|
||||||
dst []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// compress snappy-compresses the data.
|
|
||||||
func (s *snappyBuffer) compress(data []byte) []byte {
|
|
||||||
// The snappy library does not care what the capacity of the buffer is,
|
|
||||||
// but only checks the length. If the length is too small, it will
|
|
||||||
// allocate a brand new buffer.
|
|
||||||
// To avoid that, we check the required size here, and grow the size of the
|
|
||||||
// buffer to utilize the full capacity.
|
|
||||||
if n := snappy.MaxEncodedLen(len(data)); len(s.dst) < n {
|
|
||||||
if cap(s.dst) < n {
|
|
||||||
s.dst = make([]byte, n)
|
|
||||||
}
|
|
||||||
s.dst = s.dst[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
s.dst = snappy.Encode(s.dst, data)
|
|
||||||
return s.dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBuffer implements io.Writer for a byte slice.
|
|
||||||
type writeBuffer struct {
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wb *writeBuffer) Write(data []byte) (int, error) {
|
|
||||||
wb.data = append(wb.data, data...)
|
|
||||||
return len(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wb *writeBuffer) Reset() {
|
|
||||||
wb.data = wb.data[:0]
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const freezerVersion = 1 // The initial version tag of freezer table metadata
|
|
||||||
|
|
||||||
// freezerTableMeta wraps all the metadata of the freezer table.
|
|
||||||
type freezerTableMeta struct {
|
|
||||||
// Version is the versioning descriptor of the freezer table.
|
|
||||||
Version uint16
|
|
||||||
|
|
||||||
// VirtualTail indicates how many items have been marked as deleted.
|
|
||||||
// Its value is equal to the number of items removed from the table
|
|
||||||
// plus the number of items hidden in the table, so it should never
|
|
||||||
// be lower than the "actual tail".
|
|
||||||
VirtualTail uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMetadata initializes the metadata object with the given virtual tail.
|
|
||||||
func newMetadata(tail uint64) *freezerTableMeta {
|
|
||||||
return &freezerTableMeta{
|
|
||||||
Version: freezerVersion,
|
|
||||||
VirtualTail: tail,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readMetadata reads the metadata of the freezer table from the
|
|
||||||
// given metadata file.
|
|
||||||
func readMetadata(file *os.File) (*freezerTableMeta, error) {
|
|
||||||
_, err := file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var meta freezerTableMeta
|
|
||||||
if err := rlp.Decode(file, &meta); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &meta, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeMetadata writes the metadata of the freezer table into the
|
|
||||||
// given metadata file.
|
|
||||||
func writeMetadata(file *os.File, meta *freezerTableMeta) error {
|
|
||||||
_, err := file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return rlp.Encode(file, meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadMetadata loads the metadata from the given metadata file.
|
|
||||||
// Initializes the metadata file with the given "actual tail" if
|
|
||||||
// it's empty.
|
|
||||||
func loadMetadata(file *os.File, tail uint64) (*freezerTableMeta, error) {
|
|
||||||
stat, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Write the metadata with the given actual tail into metadata file
|
|
||||||
// if it's non-existent. There are two possible scenarios here:
|
|
||||||
// - the freezer table is empty
|
|
||||||
// - the freezer table is legacy
|
|
||||||
// In both cases, write the meta into the file with the actual tail
|
|
||||||
// as the virtual tail.
|
|
||||||
if stat.Size() == 0 {
|
|
||||||
m := newMetadata(tail)
|
|
||||||
if err := writeMetadata(file, m); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
m, err := readMetadata(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Update the virtual tail with the given actual tail if it's even
|
|
||||||
// lower than it. Theoretically it shouldn't happen at all, print
|
|
||||||
// a warning here.
|
|
||||||
if m.VirtualTail < tail {
|
|
||||||
log.Warn("Updated virtual tail", "have", m.VirtualTail, "now", tail)
|
|
||||||
m.VirtualTail = tail
|
|
||||||
if err := writeMetadata(file, m); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReadWriteFreezerTableMeta(t *testing.T) {
|
|
||||||
f, err := os.CreateTemp(os.TempDir(), "*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create file %v", err)
|
|
||||||
}
|
|
||||||
err = writeMetadata(f, newMetadata(100))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to write metadata %v", err)
|
|
||||||
}
|
|
||||||
meta, err := readMetadata(f)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read metadata %v", err)
|
|
||||||
}
|
|
||||||
if meta.Version != freezerVersion {
|
|
||||||
t.Fatalf("Unexpected version field")
|
|
||||||
}
|
|
||||||
if meta.VirtualTail != uint64(100) {
|
|
||||||
t.Fatalf("Unexpected virtual tail field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitializeFreezerTableMeta(t *testing.T) {
|
|
||||||
f, err := os.CreateTemp(os.TempDir(), "*")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create file %v", err)
|
|
||||||
}
|
|
||||||
meta, err := loadMetadata(f, uint64(100))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read metadata %v", err)
|
|
||||||
}
|
|
||||||
if meta.Version != freezerVersion {
|
|
||||||
t.Fatalf("Unexpected version field")
|
|
||||||
}
|
|
||||||
if meta.VirtualTail != uint64(100) {
|
|
||||||
t.Fatalf("Unexpected virtual tail field")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,238 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const tmpSuffix = ".tmp"
|
|
||||||
|
|
||||||
// freezerOpenFunc is the function used to open/create a freezer.
|
|
||||||
type freezerOpenFunc = func() (*Freezer, error)
|
|
||||||
|
|
||||||
// ResettableFreezer is a wrapper of the freezer which makes the
|
|
||||||
// freezer resettable.
|
|
||||||
type ResettableFreezer struct {
|
|
||||||
freezer *Freezer
|
|
||||||
opener freezerOpenFunc
|
|
||||||
datadir string
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewResettableFreezer creates a resettable freezer, note freezer is
|
|
||||||
// only resettable if the passed file directory is exclusively occupied
|
|
||||||
// by the freezer. And also the user-configurable ancient root directory
|
|
||||||
// is **not** supported for reset since it might be a mount and rename
|
|
||||||
// will cause a copy of hundreds of gigabyte into local directory. It
|
|
||||||
// needs some other file based solutions.
|
|
||||||
//
|
|
||||||
// The reset function will delete directory atomically and re-create the
|
|
||||||
// freezer from scratch.
|
|
||||||
func NewResettableFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*ResettableFreezer, error) {
|
|
||||||
if err := cleanup(datadir); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
opener := func() (*Freezer, error) {
|
|
||||||
return NewFreezer(datadir, namespace, readonly, maxTableSize, tables)
|
|
||||||
}
|
|
||||||
freezer, err := opener()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ResettableFreezer{
|
|
||||||
freezer: freezer,
|
|
||||||
opener: opener,
|
|
||||||
datadir: datadir,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset deletes the file directory exclusively occupied by the freezer and
|
|
||||||
// recreate the freezer from scratch. The atomicity of directory deletion
|
|
||||||
// is guaranteed by the rename operation, the leftover directory will be
|
|
||||||
// cleaned up in next startup in case crash happens after rename.
|
|
||||||
func (f *ResettableFreezer) Reset() error {
|
|
||||||
f.lock.Lock()
|
|
||||||
defer f.lock.Unlock()
|
|
||||||
|
|
||||||
if err := f.freezer.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmp := tmpName(f.datadir)
|
|
||||||
if err := os.Rename(f.datadir, tmp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.RemoveAll(tmp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
freezer, err := f.opener()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.freezer = freezer
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close terminates the chain freezer, unmapping all the data files.
|
|
||||||
func (f *ResettableFreezer) Close() error {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasAncient returns an indicator whether the specified ancient data exists
|
|
||||||
// in the freezer
|
|
||||||
func (f *ResettableFreezer) HasAncient(kind string, number uint64) (bool, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.HasAncient(kind, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancient retrieves an ancient binary blob from the append-only immutable files.
|
|
||||||
func (f *ResettableFreezer) Ancient(kind string, number uint64) ([]byte, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.Ancient(kind, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientRange retrieves multiple items in sequence, starting from the index 'start'.
|
|
||||||
// It will return
|
|
||||||
// - at most 'count' items,
|
|
||||||
// - if maxBytes is specified: at least 1 item (even if exceeding the maxByteSize),
|
|
||||||
// but will otherwise return as many items as fit into maxByteSize.
|
|
||||||
// - if maxBytes is not specified, 'count' items will be returned if they are present.
|
|
||||||
func (f *ResettableFreezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.AncientRange(kind, start, count, maxBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancients returns the length of the frozen items.
|
|
||||||
func (f *ResettableFreezer) Ancients() (uint64, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.Ancients()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tail returns the number of first stored item in the freezer.
|
|
||||||
func (f *ResettableFreezer) Tail() (uint64, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.Tail()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientSize returns the ancient size of the specified category.
|
|
||||||
func (f *ResettableFreezer) AncientSize(kind string) (uint64, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.AncientSize(kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAncients runs the given read operation while ensuring that no writes take place
|
|
||||||
// on the underlying freezer.
|
|
||||||
func (f *ResettableFreezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.ReadAncients(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyAncients runs the given write operation.
|
|
||||||
func (f *ResettableFreezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.ModifyAncients(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateHead discards any recent data above the provided threshold number.
|
|
||||||
// It returns the previous head number.
|
|
||||||
func (f *ResettableFreezer) TruncateHead(items uint64) (uint64, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.TruncateHead(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateTail discards any recent data below the provided threshold number.
|
|
||||||
// It returns the previous value
|
|
||||||
func (f *ResettableFreezer) TruncateTail(tail uint64) (uint64, error) {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.TruncateTail(tail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync flushes all data tables to disk.
|
|
||||||
func (f *ResettableFreezer) Sync() error {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MigrateTable processes the entries in a given table in sequence
|
|
||||||
// converting them to a new format if they're of an old format.
|
|
||||||
func (f *ResettableFreezer) MigrateTable(kind string, convert convertLegacyFn) error {
|
|
||||||
f.lock.RLock()
|
|
||||||
defer f.lock.RUnlock()
|
|
||||||
|
|
||||||
return f.freezer.MigrateTable(kind, convert)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup removes the directory located in the specified path
|
|
||||||
// has the name with deletion marker suffix.
|
|
||||||
func cleanup(path string) error {
|
|
||||||
parent := filepath.Dir(path)
|
|
||||||
if _, err := os.Lstat(parent); os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dir, err := os.Open(parent)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
names, err := dir.Readdirnames(0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cerr := dir.Close(); cerr != nil {
|
|
||||||
return cerr
|
|
||||||
}
|
|
||||||
for _, name := range names {
|
|
||||||
if name == filepath.Base(path)+tmpSuffix {
|
|
||||||
log.Info("Removed leftover freezer directory", "name", name)
|
|
||||||
return os.RemoveAll(filepath.Join(parent, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tmpName(path string) string {
|
|
||||||
return filepath.Join(filepath.Dir(path), filepath.Base(path)+tmpSuffix)
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResetFreezer(t *testing.T) {
|
|
||||||
items := []struct {
|
|
||||||
id uint64
|
|
||||||
blob []byte
|
|
||||||
}{
|
|
||||||
{0, bytes.Repeat([]byte{0}, 2048)},
|
|
||||||
{1, bytes.Repeat([]byte{1}, 2048)},
|
|
||||||
{2, bytes.Repeat([]byte{2}, 2048)},
|
|
||||||
}
|
|
||||||
f, _ := NewResettableFreezer(t.TempDir(), "", false, 2048, freezerTestTableDef)
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for _, item := range items {
|
|
||||||
op.AppendRaw("test", item.id, item.blob)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
for _, item := range items {
|
|
||||||
blob, _ := f.Ancient("test", item.id)
|
|
||||||
if !bytes.Equal(blob, item.blob) {
|
|
||||||
t.Fatal("Unexpected blob")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset freezer
|
|
||||||
f.Reset()
|
|
||||||
count, _ := f.Ancients()
|
|
||||||
if count != 0 {
|
|
||||||
t.Fatal("Failed to reset freezer")
|
|
||||||
}
|
|
||||||
for _, item := range items {
|
|
||||||
blob, _ := f.Ancient("test", item.id)
|
|
||||||
if len(blob) != 0 {
|
|
||||||
t.Fatal("Unexpected blob")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill the freezer
|
|
||||||
f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for _, item := range items {
|
|
||||||
op.AppendRaw("test", item.id, item.blob)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
for _, item := range items {
|
|
||||||
blob, _ := f.Ancient("test", item.id)
|
|
||||||
if !bytes.Equal(blob, item.blob) {
|
|
||||||
t.Fatal("Unexpected blob")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFreezerCleanup(t *testing.T) {
|
|
||||||
items := []struct {
|
|
||||||
id uint64
|
|
||||||
blob []byte
|
|
||||||
}{
|
|
||||||
{0, bytes.Repeat([]byte{0}, 2048)},
|
|
||||||
{1, bytes.Repeat([]byte{1}, 2048)},
|
|
||||||
{2, bytes.Repeat([]byte{2}, 2048)},
|
|
||||||
}
|
|
||||||
datadir := t.TempDir()
|
|
||||||
f, _ := NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef)
|
|
||||||
f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for _, item := range items {
|
|
||||||
op.AppendRaw("test", item.id, item.blob)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
f.Close()
|
|
||||||
os.Rename(datadir, tmpName(datadir))
|
|
||||||
|
|
||||||
// Open the freezer again, trigger cleanup operation
|
|
||||||
f, _ = NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef)
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
if _, err := os.Lstat(tmpName(datadir)); !os.IsNotExist(err) {
|
|
||||||
t.Fatal("Failed to cleanup leftover directory")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,990 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"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")
|
|
||||||
|
|
||||||
// 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, as well 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
|
|
||||||
|
|
||||||
// unmarshalBinary deserializes binary b into the rawIndex entry.
|
|
||||||
func (i *indexEntry) unmarshalBinary(b []byte) {
|
|
||||||
i.filenum = uint32(binary.BigEndian.Uint16(b[:2]))
|
|
||||||
i.offset = binary.BigEndian.Uint32(b[2:6])
|
|
||||||
}
|
|
||||||
|
|
||||||
// append adds the encoded entry to the end of b.
|
|
||||||
func (i *indexEntry) append(b []byte) []byte {
|
|
||||||
offset := len(b)
|
|
||||||
out := append(b, make([]byte, indexEntrySize)...)
|
|
||||||
binary.BigEndian.PutUint16(out[offset:], uint16(i.filenum))
|
|
||||||
binary.BigEndian.PutUint32(out[offset+2:], i.offset)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// bounds returns the start- and end- offsets, and the file number of where to
|
|
||||||
// read there data item marked by the two index entries. The two entries are
|
|
||||||
// assumed to be sequential.
|
|
||||||
func (i *indexEntry) bounds(end *indexEntry) (startOffset, endOffset, fileId uint32) {
|
|
||||||
if i.filenum != end.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, end.offset, end.filenum
|
|
||||||
}
|
|
||||||
return i.offset, end.offset, end.filenum
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 indexEntry
|
|
||||||
// file (uncompressed 64 bit indices into the data file).
|
|
||||||
type freezerTable struct {
|
|
||||||
items atomic.Uint64 // Number of items stored in the table (including items removed from tail)
|
|
||||||
itemOffset atomic.Uint64 // Number of items removed from the table
|
|
||||||
|
|
||||||
// itemHidden is the number of items marked as deleted. Tail deletion is
|
|
||||||
// only supported at file level which means the actual deletion will be
|
|
||||||
// delayed until the entire data file is marked as deleted. Before that
|
|
||||||
// these items will be hidden to prevent being visited again. The value
|
|
||||||
// should never be lower than itemOffset.
|
|
||||||
itemHidden atomic.Uint64
|
|
||||||
|
|
||||||
noCompression bool // if true, disables snappy compression. Note: does not work retroactively
|
|
||||||
readonly bool
|
|
||||||
maxFileSize uint32 // Max file size for data-files
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
|
|
||||||
head *os.File // File descriptor for the data head of the table
|
|
||||||
index *os.File // File descriptor for the indexEntry file of the table
|
|
||||||
meta *os.File // File descriptor for metadata 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
|
|
||||||
|
|
||||||
headBytes int64 // 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
|
|
||||||
sizeGauge metrics.Gauge // Gauge for tracking the combined size of all freezer tables
|
|
||||||
|
|
||||||
logger log.Logger // Logger with database path and table name embedded
|
|
||||||
lock sync.RWMutex // Mutex protecting the data file descriptors
|
|
||||||
}
|
|
||||||
|
|
||||||
// newFreezerTable opens the given path as a freezer table.
|
|
||||||
func newFreezerTable(path, name string, disableSnappy, readonly bool) (*freezerTable, error) {
|
|
||||||
return newTable(path, name, metrics.NilMeter{}, metrics.NilMeter{}, metrics.NilGauge{}, freezerTableSize, disableSnappy, readonly)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, sizeGauge metrics.Gauge, maxFilesize uint32, noCompression, readonly bool) (*freezerTable, error) {
|
|
||||||
// Ensure the containing directory exists and open the indexEntry file
|
|
||||||
if err := os.MkdirAll(path, 0755); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var idxName string
|
|
||||||
if noCompression {
|
|
||||||
idxName = fmt.Sprintf("%s.ridx", name) // raw index file
|
|
||||||
} else {
|
|
||||||
idxName = fmt.Sprintf("%s.cidx", name) // compressed index file
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
index *os.File
|
|
||||||
meta *os.File
|
|
||||||
)
|
|
||||||
if readonly {
|
|
||||||
// Will fail if table index file or meta file is not existent
|
|
||||||
index, err = openFreezerFileForReadOnly(filepath.Join(path, idxName))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
meta, err = openFreezerFileForReadOnly(filepath.Join(path, fmt.Sprintf("%s.meta", name)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
index, err = openFreezerFileForAppend(filepath.Join(path, idxName))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
meta, err = openFreezerFileForAppend(filepath.Join(path, fmt.Sprintf("%s.meta", name)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create the table and repair any past inconsistency
|
|
||||||
tab := &freezerTable{
|
|
||||||
index: index,
|
|
||||||
meta: meta,
|
|
||||||
files: make(map[uint32]*os.File),
|
|
||||||
readMeter: readMeter,
|
|
||||||
writeMeter: writeMeter,
|
|
||||||
sizeGauge: sizeGauge,
|
|
||||||
name: name,
|
|
||||||
path: path,
|
|
||||||
logger: log.New("database", path, "table", name),
|
|
||||||
noCompression: noCompression,
|
|
||||||
readonly: readonly,
|
|
||||||
maxFileSize: maxFilesize,
|
|
||||||
}
|
|
||||||
if err := tab.repair(); err != nil {
|
|
||||||
tab.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Initialize the starting size counter
|
|
||||||
size, err := tab.sizeNolock()
|
|
||||||
if err != nil {
|
|
||||||
tab.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tab.sizeGauge.Inc(int64(size))
|
|
||||||
|
|
||||||
return tab, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 indexEntry into
|
|
||||||
buffer := make([]byte, indexEntrySize)
|
|
||||||
|
|
||||||
// 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.index.Write(buffer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ensure the index is a multiple of indexEntrySize bytes
|
|
||||||
if overflow := stat.Size() % indexEntrySize; overflow != 0 {
|
|
||||||
if t.readonly {
|
|
||||||
return fmt.Errorf("index file(path: %s, name: %s) size is not a multiple of %d", t.path, t.name, indexEntrySize)
|
|
||||||
}
|
|
||||||
if err := truncateFreezerFile(t.index, stat.Size()-overflow); err != nil {
|
|
||||||
return err
|
|
||||||
} // New file can't trigger this path
|
|
||||||
}
|
|
||||||
// Retrieve the file sizes and prepare for truncation
|
|
||||||
if stat, err = t.index.Stat(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offsetsSize := stat.Size()
|
|
||||||
|
|
||||||
// Open the head file
|
|
||||||
var (
|
|
||||||
firstIndex indexEntry
|
|
||||||
lastIndex indexEntry
|
|
||||||
contentSize int64
|
|
||||||
contentExp int64
|
|
||||||
verbose bool
|
|
||||||
)
|
|
||||||
// Read index zero, determine what file is the earliest
|
|
||||||
// and what item offset to use
|
|
||||||
t.index.ReadAt(buffer, 0)
|
|
||||||
firstIndex.unmarshalBinary(buffer)
|
|
||||||
|
|
||||||
// Assign the tail fields with the first stored index.
|
|
||||||
// The total removed items is represented with an uint32,
|
|
||||||
// which is not enough in theory but enough in practice.
|
|
||||||
// TODO: use uint64 to represent total removed items.
|
|
||||||
t.tailId = firstIndex.filenum
|
|
||||||
t.itemOffset.Store(uint64(firstIndex.offset))
|
|
||||||
|
|
||||||
// Load metadata from the file
|
|
||||||
meta, err := loadMetadata(t.meta, t.itemOffset.Load())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.itemHidden.Store(meta.VirtualTail)
|
|
||||||
|
|
||||||
// Read the last index, use the default value in case the freezer is empty
|
|
||||||
if offsetsSize == indexEntrySize {
|
|
||||||
lastIndex = indexEntry{filenum: t.tailId, offset: 0}
|
|
||||||
} else {
|
|
||||||
t.index.ReadAt(buffer, offsetsSize-indexEntrySize)
|
|
||||||
lastIndex.unmarshalBinary(buffer)
|
|
||||||
}
|
|
||||||
// Print an error log if the index is corrupted due to an incorrect
|
|
||||||
// last index item. While it is theoretically possible to have a zero offset
|
|
||||||
// by storing all zero-size items, it is highly unlikely to occur in practice.
|
|
||||||
if lastIndex.offset == 0 && offsetsSize/indexEntrySize > 1 {
|
|
||||||
log.Error("Corrupted index file detected", "lastOffset", lastIndex.offset, "indexes", offsetsSize/indexEntrySize)
|
|
||||||
}
|
|
||||||
if t.readonly {
|
|
||||||
t.head, err = t.openFile(lastIndex.filenum, openFreezerFileForReadOnly)
|
|
||||||
} else {
|
|
||||||
t.head, err = t.openFile(lastIndex.filenum, openFreezerFileForAppend)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if stat, err = t.head.Stat(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
contentSize = stat.Size()
|
|
||||||
|
|
||||||
// Keep truncating both files until they come in sync
|
|
||||||
contentExp = int64(lastIndex.offset)
|
|
||||||
for contentExp != contentSize {
|
|
||||||
if t.readonly {
|
|
||||||
return fmt.Errorf("freezer table(path: %s, name: %s, num: %d) is corrupted", t.path, t.name, lastIndex.filenum)
|
|
||||||
}
|
|
||||||
verbose = true
|
|
||||||
// Truncate the head file to the last offset pointer
|
|
||||||
if contentExp < contentSize {
|
|
||||||
t.logger.Warn("Truncating dangling head", "indexed", contentExp, "stored", contentSize)
|
|
||||||
if err := truncateFreezerFile(t.head, contentExp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
contentSize = contentExp
|
|
||||||
}
|
|
||||||
// Truncate the index to point within the head file
|
|
||||||
if contentExp > contentSize {
|
|
||||||
t.logger.Warn("Truncating dangling indexes", "indexes", offsetsSize/indexEntrySize, "indexed", contentExp, "stored", contentSize)
|
|
||||||
if err := truncateFreezerFile(t.index, offsetsSize-indexEntrySize); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offsetsSize -= indexEntrySize
|
|
||||||
|
|
||||||
// Read the new head index, use the default value in case
|
|
||||||
// the freezer is already empty.
|
|
||||||
var newLastIndex indexEntry
|
|
||||||
if offsetsSize == indexEntrySize {
|
|
||||||
newLastIndex = indexEntry{filenum: t.tailId, offset: 0}
|
|
||||||
} else {
|
|
||||||
t.index.ReadAt(buffer, offsetsSize-indexEntrySize)
|
|
||||||
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)
|
|
||||||
if t.head, err = t.openFile(newLastIndex.filenum, openFreezerFileForAppend); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sync() fails for read-only files on windows.
|
|
||||||
if !t.readonly {
|
|
||||||
// Ensure all reparation changes have been written to disk
|
|
||||||
if err := t.index.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.head.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.meta.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update the item and byte counters and return
|
|
||||||
t.items.Store(t.itemOffset.Load() + uint64(offsetsSize/indexEntrySize-1)) // last indexEntry points to the end of the data file
|
|
||||||
t.headBytes = contentSize
|
|
||||||
t.headId = lastIndex.filenum
|
|
||||||
|
|
||||||
// Delete the leftover files because of head deletion
|
|
||||||
t.releaseFilesAfter(t.headId, true)
|
|
||||||
|
|
||||||
// Delete the leftover files because of tail deletion
|
|
||||||
t.releaseFilesBefore(t.tailId, true)
|
|
||||||
|
|
||||||
// Close opened files and preopen all files
|
|
||||||
if err := t.preopen(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if verbose {
|
|
||||||
t.logger.Info("Chain freezer table opened", "items", t.items.Load(), "deleted", t.itemOffset.Load(), "hidden", t.itemHidden.Load(), "tailId", t.tailId, "headId", t.headId, "size", t.headBytes)
|
|
||||||
} else {
|
|
||||||
t.logger.Debug("Chain freezer table opened", "items", t.items.Load(), "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 := t.tailId; i < t.headId; i++ {
|
|
||||||
if _, err = t.openFile(i, openFreezerFileForReadOnly); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if t.readonly {
|
|
||||||
t.head, err = t.openFile(t.headId, openFreezerFileForReadOnly)
|
|
||||||
} else {
|
|
||||||
// Open head in read/write
|
|
||||||
t.head, err = t.openFile(t.headId, openFreezerFileForAppend)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateHead discards any recent data above the provided threshold number.
|
|
||||||
func (t *freezerTable) truncateHead(items uint64) error {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
// Ensure the given truncate target falls in the correct range
|
|
||||||
existing := t.items.Load()
|
|
||||||
if existing <= items {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if items < t.itemHidden.Load() {
|
|
||||||
return errors.New("truncation below tail")
|
|
||||||
}
|
|
||||||
// We need to truncate, save the old size for metrics tracking
|
|
||||||
oldSize, err := t.sizeNolock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Something's out of sync, truncate the table's offset index
|
|
||||||
log := t.logger.Debug
|
|
||||||
if existing > items+1 {
|
|
||||||
log = t.logger.Warn // Only loud warn if we delete multiple items
|
|
||||||
}
|
|
||||||
log("Truncating freezer table", "items", existing, "limit", items)
|
|
||||||
|
|
||||||
// Truncate the index file first, the tail position is also considered
|
|
||||||
// when calculating the new freezer table length.
|
|
||||||
length := items - t.itemOffset.Load()
|
|
||||||
if err := truncateFreezerFile(t.index, int64(length+1)*indexEntrySize); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.index.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Calculate the new expected size of the data file and truncate it
|
|
||||||
var expected indexEntry
|
|
||||||
if length == 0 {
|
|
||||||
expected = indexEntry{filenum: t.tailId, offset: 0}
|
|
||||||
} else {
|
|
||||||
buffer := make([]byte, indexEntrySize)
|
|
||||||
if _, err := t.index.ReadAt(buffer, int64(length*indexEntrySize)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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, openFreezerFileForAppend)
|
|
||||||
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
|
|
||||||
t.headId = expected.filenum
|
|
||||||
}
|
|
||||||
if err := truncateFreezerFile(t.head, int64(expected.offset)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.head.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// All data files truncated, set internal counters and return
|
|
||||||
t.headBytes = int64(expected.offset)
|
|
||||||
t.items.Store(items)
|
|
||||||
|
|
||||||
// Retrieve the new size and update the total size counter
|
|
||||||
newSize, err := t.sizeNolock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.sizeGauge.Dec(int64(oldSize - newSize))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sizeHidden returns the total data size of hidden items in the freezer table.
|
|
||||||
// This function assumes the lock is already held.
|
|
||||||
func (t *freezerTable) sizeHidden() (uint64, error) {
|
|
||||||
hidden, offset := t.itemHidden.Load(), t.itemOffset.Load()
|
|
||||||
if hidden <= offset {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
indices, err := t.getIndices(hidden-1, 1)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint64(indices[1].offset), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateTail discards any recent data before the provided threshold number.
|
|
||||||
func (t *freezerTable) truncateTail(items uint64) error {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
// Ensure the given truncate target falls in the correct range
|
|
||||||
if t.itemHidden.Load() >= items {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if t.items.Load() < items {
|
|
||||||
return errors.New("truncation above head")
|
|
||||||
}
|
|
||||||
// Load the new tail index by the given new tail position
|
|
||||||
var (
|
|
||||||
newTailId uint32
|
|
||||||
buffer = make([]byte, indexEntrySize)
|
|
||||||
)
|
|
||||||
if t.items.Load() == items {
|
|
||||||
newTailId = t.headId
|
|
||||||
} else {
|
|
||||||
offset := items - t.itemOffset.Load()
|
|
||||||
if _, err := t.index.ReadAt(buffer, int64((offset+1)*indexEntrySize)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var newTail indexEntry
|
|
||||||
newTail.unmarshalBinary(buffer)
|
|
||||||
newTailId = newTail.filenum
|
|
||||||
}
|
|
||||||
// Save the old size for metrics tracking. This needs to be done
|
|
||||||
// before any updates to either itemHidden or itemOffset.
|
|
||||||
oldSize, err := t.sizeNolock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Update the virtual tail marker and hidden these entries in table.
|
|
||||||
t.itemHidden.Store(items)
|
|
||||||
if err := writeMetadata(t.meta, newMetadata(items)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Hidden items still fall in the current tail file, no data file
|
|
||||||
// can be dropped.
|
|
||||||
if t.tailId == newTailId {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Hidden items fall in the incorrect range, returns the error.
|
|
||||||
if t.tailId > newTailId {
|
|
||||||
return fmt.Errorf("invalid index, tail-file %d, item-file %d", t.tailId, newTailId)
|
|
||||||
}
|
|
||||||
// Count how many items can be deleted from the file.
|
|
||||||
var (
|
|
||||||
newDeleted = items
|
|
||||||
deleted = t.itemOffset.Load()
|
|
||||||
)
|
|
||||||
// Hidden items exceed the current tail file, drop the relevant data files.
|
|
||||||
for current := items - 1; current >= deleted; current -= 1 {
|
|
||||||
if _, err := t.index.ReadAt(buffer, int64((current-deleted+1)*indexEntrySize)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var pre indexEntry
|
|
||||||
pre.unmarshalBinary(buffer)
|
|
||||||
if pre.filenum != newTailId {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
newDeleted = current
|
|
||||||
}
|
|
||||||
// Commit the changes of metadata file first before manipulating
|
|
||||||
// the indexes file.
|
|
||||||
if err := t.meta.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Close the index file before shorten it.
|
|
||||||
if err := t.index.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Truncate the deleted index entries from the index file.
|
|
||||||
err = copyFrom(t.index.Name(), t.index.Name(), indexEntrySize*(newDeleted-deleted+1), func(f *os.File) error {
|
|
||||||
tailIndex := indexEntry{
|
|
||||||
filenum: newTailId,
|
|
||||||
offset: uint32(newDeleted),
|
|
||||||
}
|
|
||||||
_, err := f.Write(tailIndex.append(nil))
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Reopen the modified index file to load the changes
|
|
||||||
t.index, err = openFreezerFileForAppend(t.index.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Sync the file to ensure changes are flushed to disk
|
|
||||||
if err := t.index.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Release any files before the current tail
|
|
||||||
t.tailId = newTailId
|
|
||||||
t.itemOffset.Store(newDeleted)
|
|
||||||
t.releaseFilesBefore(t.tailId, true)
|
|
||||||
|
|
||||||
// Retrieve the new size and update the total size counter
|
|
||||||
newSize, err := t.sizeNolock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.sizeGauge.Dec(int64(oldSize - newSize))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes all opened files.
|
|
||||||
func (t *freezerTable) Close() error {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
doClose := func(f *os.File, sync bool, close bool) {
|
|
||||||
if sync && !t.readonly {
|
|
||||||
if err := f.Sync(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if close {
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Trying to fsync a file opened in rdonly causes "Access denied"
|
|
||||||
// error on Windows.
|
|
||||||
doClose(t.index, true, true)
|
|
||||||
doClose(t.meta, true, true)
|
|
||||||
|
|
||||||
// The preopened non-head data-files are all opened in readonly.
|
|
||||||
// The head is opened in rw-mode, so we sync it here - but since it's also
|
|
||||||
// part of t.files, it will be closed in the loop below.
|
|
||||||
doClose(t.head, true, false) // sync but do not close
|
|
||||||
|
|
||||||
for _, f := range t.files {
|
|
||||||
doClose(f, false, true) // close but do not sync
|
|
||||||
}
|
|
||||||
t.index = nil
|
|
||||||
t.meta = nil
|
|
||||||
t.head = nil
|
|
||||||
|
|
||||||
if errs != nil {
|
|
||||||
return fmt.Errorf("%v", errs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFile assumes that the write-lock is held by the caller
|
|
||||||
func (t *freezerTable) openFile(num uint32, opener func(string) (*os.File, error)) (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.%04d.rdat", t.name, num)
|
|
||||||
} else {
|
|
||||||
name = fmt.Sprintf("%s.%04d.cdat", t.name, num)
|
|
||||||
}
|
|
||||||
f, err = opener(filepath.Join(t.path, name))
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// releaseFilesBefore closes all open files with a lower number, and optionally also deletes the files
|
|
||||||
func (t *freezerTable) releaseFilesBefore(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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getIndices returns the index entries for the given from-item, covering 'count' items.
|
|
||||||
// N.B: The actual number of returned indices for N items will always be N+1 (unless an
|
|
||||||
// error is returned).
|
|
||||||
// OBS: This method assumes that the caller has already verified (and/or trimmed) the range
|
|
||||||
// so that the items are within bounds. If this method is used to read out of bounds,
|
|
||||||
// it will return error.
|
|
||||||
func (t *freezerTable) getIndices(from, count uint64) ([]*indexEntry, error) {
|
|
||||||
// Apply the table-offset
|
|
||||||
from = from - t.itemOffset.Load()
|
|
||||||
|
|
||||||
// For reading N items, we need N+1 indices.
|
|
||||||
buffer := make([]byte, (count+1)*indexEntrySize)
|
|
||||||
if _, err := t.index.ReadAt(buffer, int64(from*indexEntrySize)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
indices []*indexEntry
|
|
||||||
offset int
|
|
||||||
)
|
|
||||||
for i := from; i <= from+count; i++ {
|
|
||||||
index := new(indexEntry)
|
|
||||||
index.unmarshalBinary(buffer[offset:])
|
|
||||||
offset += indexEntrySize
|
|
||||||
indices = append(indices, index)
|
|
||||||
}
|
|
||||||
if from == 0 {
|
|
||||||
// Special case if we're reading the first item in the freezer. We assume that
|
|
||||||
// the first item always start from zero(regarding the deletion, we
|
|
||||||
// only support deletion by files, so that the assumption is held).
|
|
||||||
// This means we can use the first item metadata to carry information about
|
|
||||||
// the 'global' offset, for the deletion-case
|
|
||||||
indices[0].offset = 0
|
|
||||||
indices[0].filenum = indices[1].filenum
|
|
||||||
}
|
|
||||||
return indices, 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) {
|
|
||||||
items, err := t.RetrieveItems(item, 1, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetrieveItems returns multiple items in sequence, starting from the index 'start'.
|
|
||||||
// It will return at most 'max' items, but will abort earlier to respect the
|
|
||||||
// 'maxBytes' argument. However, if the 'maxBytes' is smaller than the size of one
|
|
||||||
// item, it _will_ return one element and possibly overflow the maxBytes.
|
|
||||||
func (t *freezerTable) RetrieveItems(start, count, maxBytes uint64) ([][]byte, error) {
|
|
||||||
// First we read the 'raw' data, which might be compressed.
|
|
||||||
diskData, sizes, err := t.retrieveItems(start, count, maxBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
output = make([][]byte, 0, count)
|
|
||||||
offset int // offset for reading
|
|
||||||
outputSize int // size of uncompressed data
|
|
||||||
)
|
|
||||||
// Now slice up the data and decompress.
|
|
||||||
for i, diskSize := range sizes {
|
|
||||||
item := diskData[offset : offset+diskSize]
|
|
||||||
offset += diskSize
|
|
||||||
decompressedSize := diskSize
|
|
||||||
if !t.noCompression {
|
|
||||||
decompressedSize, _ = snappy.DecodedLen(item)
|
|
||||||
}
|
|
||||||
if i > 0 && maxBytes != 0 && uint64(outputSize+decompressedSize) > maxBytes {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !t.noCompression {
|
|
||||||
data, err := snappy.Decode(nil, item)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
output = append(output, data)
|
|
||||||
} else {
|
|
||||||
output = append(output, item)
|
|
||||||
}
|
|
||||||
outputSize += decompressedSize
|
|
||||||
}
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieveItems reads up to 'count' items from the table. It reads at least
|
|
||||||
// one item, but otherwise avoids reading more than maxBytes bytes. Freezer
|
|
||||||
// will ignore the size limitation and continuously allocate memory to store
|
|
||||||
// data if maxBytes is 0. It returns the (potentially compressed) data, and
|
|
||||||
// the sizes.
|
|
||||||
func (t *freezerTable) retrieveItems(start, count, maxBytes uint64) ([]byte, []int, error) {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
// Ensure the table and the item are accessible
|
|
||||||
if t.index == nil || t.head == nil || t.meta == nil {
|
|
||||||
return nil, nil, errClosed
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
items = t.items.Load() // the total items(head + 1)
|
|
||||||
hidden = t.itemHidden.Load() // the number of hidden items
|
|
||||||
)
|
|
||||||
// Ensure the start is written, not deleted from the tail, and that the
|
|
||||||
// caller actually wants something
|
|
||||||
if items <= start || hidden > start || count == 0 {
|
|
||||||
return nil, nil, errOutOfBounds
|
|
||||||
}
|
|
||||||
if start+count > items {
|
|
||||||
count = items - start
|
|
||||||
}
|
|
||||||
var output []byte // Buffer to read data into
|
|
||||||
if maxBytes != 0 {
|
|
||||||
output = make([]byte, 0, maxBytes)
|
|
||||||
} else {
|
|
||||||
output = make([]byte, 0, 1024) // initial buffer cap
|
|
||||||
}
|
|
||||||
// readData is a helper method to read a single data item from disk.
|
|
||||||
readData := func(fileId, start uint32, length int) error {
|
|
||||||
output = grow(output, length)
|
|
||||||
dataFile, exist := t.files[fileId]
|
|
||||||
if !exist {
|
|
||||||
return fmt.Errorf("missing data file %d", fileId)
|
|
||||||
}
|
|
||||||
if _, err := dataFile.ReadAt(output[len(output)-length:], int64(start)); err != nil {
|
|
||||||
return fmt.Errorf("%w, fileid: %d, start: %d, length: %d", err, fileId, start, length)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Read all the indexes in one go
|
|
||||||
indices, err := t.getIndices(start, count)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
sizes []int // The sizes for each element
|
|
||||||
totalSize = 0 // The total size of all data read so far
|
|
||||||
readStart = indices[0].offset // Where, in the file, to start reading
|
|
||||||
unreadSize = 0 // The size of the as-yet-unread data
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, firstIndex := range indices[:len(indices)-1] {
|
|
||||||
secondIndex := indices[i+1]
|
|
||||||
// Determine the size of the item.
|
|
||||||
offset1, offset2, _ := firstIndex.bounds(secondIndex)
|
|
||||||
size := int(offset2 - offset1)
|
|
||||||
// Crossing a file boundary?
|
|
||||||
if secondIndex.filenum != firstIndex.filenum {
|
|
||||||
// If we have unread data in the first file, we need to do that read now.
|
|
||||||
if unreadSize > 0 {
|
|
||||||
if err := readData(firstIndex.filenum, readStart, unreadSize); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
unreadSize = 0
|
|
||||||
}
|
|
||||||
readStart = 0
|
|
||||||
}
|
|
||||||
if i > 0 && uint64(totalSize+size) > maxBytes && maxBytes != 0 {
|
|
||||||
// About to break out due to byte limit being exceeded. We don't
|
|
||||||
// read this last item, but we need to do the deferred reads now.
|
|
||||||
if unreadSize > 0 {
|
|
||||||
if err := readData(secondIndex.filenum, readStart, unreadSize); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Defer the read for later
|
|
||||||
unreadSize += size
|
|
||||||
totalSize += size
|
|
||||||
sizes = append(sizes, size)
|
|
||||||
if i == len(indices)-2 || (uint64(totalSize) > maxBytes && maxBytes != 0) {
|
|
||||||
// Last item, need to do the read now
|
|
||||||
if err := readData(secondIndex.filenum, readStart, unreadSize); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update metrics.
|
|
||||||
t.readMeter.Mark(int64(totalSize))
|
|
||||||
return output, sizes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// has returns an indicator whether the specified number data is still accessible
|
|
||||||
// in the freezer table.
|
|
||||||
func (t *freezerTable) has(number uint64) bool {
|
|
||||||
return t.items.Load() > number && t.itemHidden.Load() <= number
|
|
||||||
}
|
|
||||||
|
|
||||||
// size returns the total data size in the freezer table.
|
|
||||||
func (t *freezerTable) size() (uint64, error) {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
return t.sizeNolock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// sizeNolock returns the total data size in the freezer table. This function
|
|
||||||
// assumes the lock is already held.
|
|
||||||
func (t *freezerTable) sizeNolock() (uint64, error) {
|
|
||||||
stat, err := t.index.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
hidden, err := t.sizeHidden()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
total := uint64(t.maxFileSize)*uint64(t.headId-t.tailId) + uint64(t.headBytes) + uint64(stat.Size()) - hidden
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// advanceHead should be called when the current head file would outgrow the file limits,
|
|
||||||
// and a new file must be opened. The caller of this method must hold the write-lock
|
|
||||||
// before calling this method.
|
|
||||||
func (t *freezerTable) advanceHead() error {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
// We open the next file in truncated mode -- if this file already
|
|
||||||
// exists, we need to start over from scratch on it.
|
|
||||||
nextID := t.headId + 1
|
|
||||||
newHead, err := t.openFile(nextID, openFreezerFileTruncated)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Commit the contents of the old file to stable storage and
|
|
||||||
// tear it down. It will be re-opened in read-only mode.
|
|
||||||
if err := t.head.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.releaseFile(t.headId)
|
|
||||||
t.openFile(t.headId, openFreezerFileForReadOnly)
|
|
||||||
|
|
||||||
// Swap out the current head.
|
|
||||||
t.head = newHead
|
|
||||||
t.headBytes = 0
|
|
||||||
t.headId = nextID
|
|
||||||
return 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 {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
if t.index == nil || t.head == nil || t.meta == nil {
|
|
||||||
return errClosed
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
trackError := func(e error) {
|
|
||||||
if e != nil && err == nil {
|
|
||||||
err = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trackError(t.index.Sync())
|
|
||||||
trackError(t.meta.Sync())
|
|
||||||
trackError(t.head.Sync())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *freezerTable) dumpIndexStdout(start, stop int64) {
|
|
||||||
t.dumpIndex(os.Stdout, start, stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *freezerTable) dumpIndexString(start, stop int64) string {
|
|
||||||
var out bytes.Buffer
|
|
||||||
out.WriteString("\n")
|
|
||||||
t.dumpIndex(&out, start, stop)
|
|
||||||
return out.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *freezerTable) dumpIndex(w io.Writer, start, stop int64) {
|
|
||||||
meta, err := readMetadata(t.meta)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(w, "Failed to decode freezer table %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "Version %d count %d, deleted %d, hidden %d\n", meta.Version,
|
|
||||||
t.items.Load(), t.itemOffset.Load(), t.itemHidden.Load())
|
|
||||||
|
|
||||||
buf := make([]byte, indexEntrySize)
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "| number | fileno | offset |\n")
|
|
||||||
fmt.Fprintf(w, "|--------|--------|--------|\n")
|
|
||||||
|
|
||||||
for i := uint64(start); ; i++ {
|
|
||||||
if _, err := t.index.ReadAt(buf, int64((i+1)*indexEntrySize)); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
var entry indexEntry
|
|
||||||
entry.unmarshalBinary(buf)
|
|
||||||
fmt.Fprintf(w, "| %03d | %03d | %03d | \n", i, entry.filenum, entry.offset)
|
|
||||||
if stop > 0 && i >= uint64(stop) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "|--------------------------|\n")
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,482 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
var freezerTestTableDef = map[string]bool{"test": true}
|
|
||||||
|
|
||||||
func TestFreezerModify(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Create test data.
|
|
||||||
var valuesRaw [][]byte
|
|
||||||
var valuesRLP []*big.Int
|
|
||||||
for x := 0; x < 100; x++ {
|
|
||||||
v := getChunk(256, x)
|
|
||||||
valuesRaw = append(valuesRaw, v)
|
|
||||||
iv := big.NewInt(int64(x))
|
|
||||||
iv = iv.Exp(iv, iv, nil)
|
|
||||||
valuesRLP = append(valuesRLP, iv)
|
|
||||||
}
|
|
||||||
|
|
||||||
tables := map[string]bool{"raw": true, "rlp": false}
|
|
||||||
f, _ := newFreezerForTesting(t, tables)
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// Commit test data.
|
|
||||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for i := range valuesRaw {
|
|
||||||
if err := op.AppendRaw("raw", uint64(i), valuesRaw[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := op.Append("rlp", uint64(i), valuesRLP[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("ModifyAncients failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dump indexes.
|
|
||||||
for _, table := range f.tables {
|
|
||||||
t.Log(table.name, "index:", table.dumpIndexString(0, int64(len(valuesRaw))))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read back test data.
|
|
||||||
checkAncientCount(t, f, "raw", uint64(len(valuesRaw)))
|
|
||||||
checkAncientCount(t, f, "rlp", uint64(len(valuesRLP)))
|
|
||||||
for i := range valuesRaw {
|
|
||||||
v, _ := f.Ancient("raw", uint64(i))
|
|
||||||
if !bytes.Equal(v, valuesRaw[i]) {
|
|
||||||
t.Fatalf("wrong raw value at %d: %x", i, v)
|
|
||||||
}
|
|
||||||
ivEnc, _ := f.Ancient("rlp", uint64(i))
|
|
||||||
want, _ := rlp.EncodeToBytes(valuesRLP[i])
|
|
||||||
if !bytes.Equal(ivEnc, want) {
|
|
||||||
t.Fatalf("wrong RLP value at %d: %x", i, ivEnc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This checks that ModifyAncients rolls back freezer updates
|
|
||||||
// when the function passed to it returns an error.
|
|
||||||
func TestFreezerModifyRollback(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
f, dir := newFreezerForTesting(t, freezerTestTableDef)
|
|
||||||
|
|
||||||
theError := errors.New("oops")
|
|
||||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
// Append three items. This creates two files immediately,
|
|
||||||
// because the table size limit of the test freezer is 2048.
|
|
||||||
require.NoError(t, op.AppendRaw("test", 0, make([]byte, 2048)))
|
|
||||||
require.NoError(t, op.AppendRaw("test", 1, make([]byte, 2048)))
|
|
||||||
require.NoError(t, op.AppendRaw("test", 2, make([]byte, 2048)))
|
|
||||||
return theError
|
|
||||||
})
|
|
||||||
if err != theError {
|
|
||||||
t.Errorf("ModifyAncients returned wrong error %q", err)
|
|
||||||
}
|
|
||||||
checkAncientCount(t, f, "test", 0)
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
// Reopen and check that the rolled-back data doesn't reappear.
|
|
||||||
tables := map[string]bool{"test": true}
|
|
||||||
f2, err := NewFreezer(dir, "", false, 2049, tables)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't reopen freezer after failed ModifyAncients: %v", err)
|
|
||||||
}
|
|
||||||
defer f2.Close()
|
|
||||||
checkAncientCount(t, f2, "test", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test runs ModifyAncients and Ancient concurrently with each other.
|
|
||||||
func TestFreezerConcurrentModifyRetrieve(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
f, _ := newFreezerForTesting(t, freezerTestTableDef)
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var (
|
|
||||||
numReaders = 5
|
|
||||||
writeBatchSize = uint64(50)
|
|
||||||
written = make(chan uint64, numReaders*6)
|
|
||||||
wg sync.WaitGroup
|
|
||||||
)
|
|
||||||
wg.Add(numReaders + 1)
|
|
||||||
|
|
||||||
// Launch the writer. It appends 10000 items in batches.
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
defer close(written)
|
|
||||||
for item := uint64(0); item < 10000; item += writeBatchSize {
|
|
||||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for i := uint64(0); i < writeBatchSize; i++ {
|
|
||||||
item := item + i
|
|
||||||
value := getChunk(32, int(item))
|
|
||||||
if err := op.AppendRaw("test", item, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
for i := 0; i < numReaders; i++ {
|
|
||||||
written <- item + writeBatchSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Launch the readers. They read random items from the freezer up to the
|
|
||||||
// current frozen item count.
|
|
||||||
for i := 0; i < numReaders; i++ {
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for frozen := range written {
|
|
||||||
for rc := 0; rc < 80; rc++ {
|
|
||||||
num := uint64(rand.Intn(int(frozen)))
|
|
||||||
value, err := f.Ancient("test", num)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("error reading %d (frozen %d): %v", num, frozen, err))
|
|
||||||
}
|
|
||||||
if !bytes.Equal(value, getChunk(32, int(num))) {
|
|
||||||
panic(fmt.Errorf("wrong value at %d", num))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test runs ModifyAncients and TruncateHead concurrently with each other.
|
|
||||||
func TestFreezerConcurrentModifyTruncate(t *testing.T) {
|
|
||||||
f, _ := newFreezerForTesting(t, freezerTestTableDef)
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var item = make([]byte, 256)
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
// First reset and write 100 items.
|
|
||||||
if _, err := f.TruncateHead(0); err != nil {
|
|
||||||
t.Fatal("truncate failed:", err)
|
|
||||||
}
|
|
||||||
_, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for i := uint64(0); i < 100; i++ {
|
|
||||||
if err := op.AppendRaw("test", i, item); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("modify failed:", err)
|
|
||||||
}
|
|
||||||
checkAncientCount(t, f, "test", 100)
|
|
||||||
|
|
||||||
// Now append 100 more items and truncate concurrently.
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
truncateErr error
|
|
||||||
modifyErr error
|
|
||||||
)
|
|
||||||
wg.Add(3)
|
|
||||||
go func() {
|
|
||||||
_, modifyErr = f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
|
|
||||||
for i := uint64(100); i < 200; i++ {
|
|
||||||
if err := op.AppendRaw("test", i, item); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
_, truncateErr = f.TruncateHead(10)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
f.AncientSize("test")
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Now check the outcome. If the truncate operation went through first, the append
|
|
||||||
// fails, otherwise it succeeds. In either case, the freezer should be positioned
|
|
||||||
// at 10 after both operations are done.
|
|
||||||
if truncateErr != nil {
|
|
||||||
t.Fatal("concurrent truncate failed:", err)
|
|
||||||
}
|
|
||||||
if !(errors.Is(modifyErr, nil) || errors.Is(modifyErr, errOutOrderInsertion)) {
|
|
||||||
t.Fatal("wrong error from concurrent modify:", modifyErr)
|
|
||||||
}
|
|
||||||
checkAncientCount(t, f, "test", 10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFreezerReadonlyValidate(t *testing.T) {
|
|
||||||
tables := map[string]bool{"a": true, "b": true}
|
|
||||||
dir := t.TempDir()
|
|
||||||
// Open non-readonly freezer and fill individual tables
|
|
||||||
// with different amount of data.
|
|
||||||
f, err := NewFreezer(dir, "", false, 2049, tables)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("can't open freezer", err)
|
|
||||||
}
|
|
||||||
var item = make([]byte, 1024)
|
|
||||||
aBatch := f.tables["a"].newBatch()
|
|
||||||
require.NoError(t, aBatch.AppendRaw(0, item))
|
|
||||||
require.NoError(t, aBatch.AppendRaw(1, item))
|
|
||||||
require.NoError(t, aBatch.AppendRaw(2, item))
|
|
||||||
require.NoError(t, aBatch.commit())
|
|
||||||
bBatch := f.tables["b"].newBatch()
|
|
||||||
require.NoError(t, bBatch.AppendRaw(0, item))
|
|
||||||
require.NoError(t, bBatch.commit())
|
|
||||||
if f.tables["a"].items.Load() != 3 {
|
|
||||||
t.Fatalf("unexpected number of items in table")
|
|
||||||
}
|
|
||||||
if f.tables["b"].items.Load() != 1 {
|
|
||||||
t.Fatalf("unexpected number of items in table")
|
|
||||||
}
|
|
||||||
require.NoError(t, f.Close())
|
|
||||||
|
|
||||||
// Re-openening as readonly should fail when validating
|
|
||||||
// table lengths.
|
|
||||||
_, err = NewFreezer(dir, "", true, 2049, tables)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("readonly freezer should fail with differing table lengths")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFreezerConcurrentReadonly(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tables := map[string]bool{"a": true}
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
f, err := NewFreezer(dir, "", false, 2049, tables)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("can't open freezer", err)
|
|
||||||
}
|
|
||||||
var item = make([]byte, 1024)
|
|
||||||
batch := f.tables["a"].newBatch()
|
|
||||||
items := uint64(10)
|
|
||||||
for i := uint64(0); i < items; i++ {
|
|
||||||
require.NoError(t, batch.AppendRaw(i, item))
|
|
||||||
}
|
|
||||||
require.NoError(t, batch.commit())
|
|
||||||
if loaded := f.tables["a"].items.Load(); loaded != items {
|
|
||||||
t.Fatalf("unexpected number of items in table, want: %d, have: %d", items, loaded)
|
|
||||||
}
|
|
||||||
require.NoError(t, f.Close())
|
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
fs = make([]*Freezer, 5)
|
|
||||||
errs = make([]error, 5)
|
|
||||||
)
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
f, err := NewFreezer(dir, "", true, 2049, tables)
|
|
||||||
if err == nil {
|
|
||||||
fs[i] = f
|
|
||||||
} else {
|
|
||||||
errs[i] = err
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for i := range fs {
|
|
||||||
if err := errs[i]; err != nil {
|
|
||||||
t.Fatal("failed to open freezer", err)
|
|
||||||
}
|
|
||||||
require.NoError(t, fs[i].Close())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFreezerForTesting(t *testing.T, tables map[string]bool) (*Freezer, string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
// note: using low max table size here to ensure the tests actually
|
|
||||||
// switch between multiple files.
|
|
||||||
f, err := NewFreezer(dir, "", false, 2049, tables)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("can't open freezer", err)
|
|
||||||
}
|
|
||||||
return f, dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAncientCount verifies that the freezer contains n items.
|
|
||||||
func checkAncientCount(t *testing.T, f *Freezer, kind string, n uint64) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if frozen, _ := f.Ancients(); frozen != n {
|
|
||||||
t.Fatalf("Ancients() returned %d, want %d", frozen, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check at index n-1.
|
|
||||||
if n > 0 {
|
|
||||||
index := n - 1
|
|
||||||
if ok, _ := f.HasAncient(kind, index); !ok {
|
|
||||||
t.Errorf("HasAncient(%q, %d) returned false unexpectedly", kind, index)
|
|
||||||
}
|
|
||||||
if _, err := f.Ancient(kind, index); err != nil {
|
|
||||||
t.Errorf("Ancient(%q, %d) returned unexpected error %q", kind, index, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check at index n.
|
|
||||||
index := n
|
|
||||||
if ok, _ := f.HasAncient(kind, index); ok {
|
|
||||||
t.Errorf("HasAncient(%q, %d) returned true unexpectedly", kind, index)
|
|
||||||
}
|
|
||||||
if _, err := f.Ancient(kind, index); err == nil {
|
|
||||||
t.Errorf("Ancient(%q, %d) didn't return expected error", kind, index)
|
|
||||||
} else if err != errOutOfBounds {
|
|
||||||
t.Errorf("Ancient(%q, %d) returned unexpected error %q", kind, index, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenameWindows(t *testing.T) {
|
|
||||||
var (
|
|
||||||
fname = "file.bin"
|
|
||||||
fname2 = "file2.bin"
|
|
||||||
data = []byte{1, 2, 3, 4}
|
|
||||||
data2 = []byte{2, 3, 4, 5}
|
|
||||||
data3 = []byte{3, 5, 6, 7}
|
|
||||||
dataLen = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create 2 temp dirs
|
|
||||||
dir1 := t.TempDir()
|
|
||||||
dir2 := t.TempDir()
|
|
||||||
|
|
||||||
// Create file in dir1 and fill with data
|
|
||||||
f, err := os.Create(path.Join(dir1, fname))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f2, err := os.Create(path.Join(dir1, fname2))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f3, err := os.Create(path.Join(dir2, fname2))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := f.Write(data); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := f2.Write(data2); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := f3.Write(data3); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := f2.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := f3.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.Rename(f.Name(), path.Join(dir2, fname)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.Rename(f2.Name(), path.Join(dir2, fname2)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file contents
|
|
||||||
f, err = os.Open(path.Join(dir2, fname))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
buf := make([]byte, dataLen)
|
|
||||||
if _, err := f.Read(buf); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(buf, data) {
|
|
||||||
t.Errorf("unexpected file contents. Got %v\n", buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err = os.Open(path.Join(dir2, fname2))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
if _, err := f.Read(buf); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(buf, data2) {
|
|
||||||
t.Errorf("unexpected file contents. Got %v\n", buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFreezerCloseSync(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
f, _ := newFreezerForTesting(t, map[string]bool{"a": true, "b": true})
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// Now, close and sync. This mimics the behaviour if the node is shut down,
|
|
||||||
// just as the chain freezer is writing.
|
|
||||||
// 1: thread-1: chain treezer writes, via freezeRange (holds lock)
|
|
||||||
// 2: thread-2: Close called, waits for write to finish
|
|
||||||
// 3: thread-1: finishes writing, releases lock
|
|
||||||
// 4: thread-2: obtains lock, completes Close()
|
|
||||||
// 5: thread-1: calls f.Sync()
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := f.Sync(); err == nil {
|
|
||||||
t.Fatalf("want error, have nil")
|
|
||||||
} else if have, want := err.Error(), "[closed closed]"; have != want {
|
|
||||||
t.Fatalf("want %v, have %v", have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// copyFrom copies data from 'srcPath' at offset 'offset' into 'destPath'.
|
|
||||||
// The 'destPath' is created if it doesn't exist, otherwise it is overwritten.
|
|
||||||
// Before the copy is executed, there is a callback can be registered to
|
|
||||||
// manipulate the dest file.
|
|
||||||
// It is perfectly valid to have destPath == srcPath.
|
|
||||||
func copyFrom(srcPath, destPath string, offset uint64, before func(f *os.File) error) error {
|
|
||||||
// Create a temp file in the same dir where we want it to wind up
|
|
||||||
f, err := os.CreateTemp(filepath.Dir(destPath), "*")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fname := f.Name()
|
|
||||||
|
|
||||||
// Clean up the leftover file
|
|
||||||
defer func() {
|
|
||||||
if f != nil {
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
os.Remove(fname)
|
|
||||||
}()
|
|
||||||
// Apply the given function if it's not nil before we copy
|
|
||||||
// the content from the src.
|
|
||||||
if before != nil {
|
|
||||||
if err := before(f); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Open the source file
|
|
||||||
src, err := os.Open(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err = src.Seek(int64(offset), 0); err != nil {
|
|
||||||
src.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// io.Copy uses 32K buffer internally.
|
|
||||||
_, err = io.Copy(f, src)
|
|
||||||
if err != nil {
|
|
||||||
src.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Rename the temporary file to the specified dest name.
|
|
||||||
// src may be same as dest, so needs to be closed before
|
|
||||||
// we do the final move.
|
|
||||||
src.Close()
|
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f = nil
|
|
||||||
return os.Rename(fname, destPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFreezerFileForAppend opens a freezer table file and seeks to the end
|
|
||||||
func openFreezerFileForAppend(filename string) (*os.File, error) {
|
|
||||||
// Open the file without the O_APPEND flag
|
|
||||||
// because it has differing behaviour during Truncate operations
|
|
||||||
// on different OS's
|
|
||||||
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Seek to end for append
|
|
||||||
if _, err = file.Seek(0, io.SeekEnd); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFreezerFileForReadOnly opens a freezer table file for read only access
|
|
||||||
func openFreezerFileForReadOnly(filename string) (*os.File, error) {
|
|
||||||
return os.OpenFile(filename, os.O_RDONLY, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFreezerFileTruncated opens a freezer table making sure it is truncated
|
|
||||||
func openFreezerFileTruncated(filename string) (*os.File, error) {
|
|
||||||
return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateFreezerFile resizes a freezer table file and seeks to the end
|
|
||||||
func truncateFreezerFile(file *os.File, size int64) error {
|
|
||||||
if err := file.Truncate(size); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Seek to end for append
|
|
||||||
if _, err := file.Seek(0, io.SeekEnd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// grow prepares the slice space for new item, and doubles the slice capacity
|
|
||||||
// if space is not enough.
|
|
||||||
func grow(buf []byte, n int) []byte {
|
|
||||||
if cap(buf)-len(buf) < n {
|
|
||||||
newcap := 2 * cap(buf)
|
|
||||||
if newcap-len(buf) < n {
|
|
||||||
newcap = len(buf) + n
|
|
||||||
}
|
|
||||||
nbuf := make([]byte, len(buf), newcap)
|
|
||||||
copy(nbuf, buf)
|
|
||||||
buf = nbuf
|
|
||||||
}
|
|
||||||
buf = buf[:len(buf)+n]
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCopyFrom(t *testing.T) {
|
|
||||||
var (
|
|
||||||
content = []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8}
|
|
||||||
prefix = []byte{0x9, 0xa, 0xb, 0xc, 0xd, 0xf}
|
|
||||||
)
|
|
||||||
var cases = []struct {
|
|
||||||
src, dest string
|
|
||||||
offset uint64
|
|
||||||
writePrefix bool
|
|
||||||
}{
|
|
||||||
{"foo", "bar", 0, false},
|
|
||||||
{"foo", "bar", 1, false},
|
|
||||||
{"foo", "bar", 8, false},
|
|
||||||
{"foo", "foo", 0, false},
|
|
||||||
{"foo", "foo", 1, false},
|
|
||||||
{"foo", "foo", 8, false},
|
|
||||||
{"foo", "bar", 0, true},
|
|
||||||
{"foo", "bar", 1, true},
|
|
||||||
{"foo", "bar", 8, true},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
os.WriteFile(c.src, content, 0600)
|
|
||||||
|
|
||||||
if err := copyFrom(c.src, c.dest, c.offset, func(f *os.File) error {
|
|
||||||
if !c.writePrefix {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
f.Write(prefix)
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
os.Remove(c.src)
|
|
||||||
t.Fatalf("Failed to copy %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
blob, err := os.ReadFile(c.dest)
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(c.src)
|
|
||||||
os.Remove(c.dest)
|
|
||||||
t.Fatalf("Failed to read %v", err)
|
|
||||||
}
|
|
||||||
want := content[c.offset:]
|
|
||||||
if c.writePrefix {
|
|
||||||
want = append(prefix, want...)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(blob, want) {
|
|
||||||
t.Fatal("Unexpected value")
|
|
||||||
}
|
|
||||||
os.Remove(c.src)
|
|
||||||
os.Remove(c.dest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import "github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
|
|
||||||
// KeyLengthIterator is a wrapper for a database iterator that ensures only key-value pairs
|
|
||||||
// with a specific key length will be returned.
|
|
||||||
type KeyLengthIterator struct {
|
|
||||||
requiredKeyLength int
|
|
||||||
ethdb.Iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewKeyLengthIterator returns a wrapped version of the iterator that will only return key-value
|
|
||||||
// pairs where keys with a specific key length will be returned.
|
|
||||||
func NewKeyLengthIterator(it ethdb.Iterator, keyLen int) ethdb.Iterator {
|
|
||||||
return &KeyLengthIterator{
|
|
||||||
Iterator: it,
|
|
||||||
requiredKeyLength: keyLen,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *KeyLengthIterator) Next() bool {
|
|
||||||
// Return true as soon as a key with the required key length is discovered
|
|
||||||
for it.Iterator.Next() {
|
|
||||||
if len(it.Iterator.Key()) == it.requiredKeyLength {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return false when we exhaust the keys in the underlying iterator.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestKeyLengthIterator(t *testing.T) {
|
|
||||||
db := NewMemoryDatabase()
|
|
||||||
|
|
||||||
keyLen := 8
|
|
||||||
expectedKeys := make(map[string]struct{})
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
key := make([]byte, keyLen)
|
|
||||||
binary.BigEndian.PutUint64(key, uint64(i))
|
|
||||||
if err := db.Put(key, []byte{0x1}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
expectedKeys[string(key)] = struct{}{}
|
|
||||||
|
|
||||||
longerKey := make([]byte, keyLen*2)
|
|
||||||
binary.BigEndian.PutUint64(longerKey, uint64(i))
|
|
||||||
if err := db.Put(longerKey, []byte{0x1}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it := NewKeyLengthIterator(db.NewIterator(nil, nil), keyLen)
|
|
||||||
for it.Next() {
|
|
||||||
key := it.Key()
|
|
||||||
_, exists := expectedKeys[string(key)]
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("Found unexpected key %d", binary.BigEndian.Uint64(key))
|
|
||||||
}
|
|
||||||
delete(expectedKeys, string(key))
|
|
||||||
if len(key) != keyLen {
|
|
||||||
t.Fatalf("Found unexpected key in key length iterator with length %d", len(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(expectedKeys) != 0 {
|
|
||||||
t.Fatalf("Expected all keys of length %d to be removed from expected keys during iteration", keyLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,339 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package rawdb contains a collection of low level database accessors.
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The fields below define the low level database schema prefixing.
|
|
||||||
var (
|
|
||||||
// databaseVersionKey tracks the current database version.
|
|
||||||
databaseVersionKey = []byte("DatabaseVersion")
|
|
||||||
|
|
||||||
// headHeaderKey tracks the latest known header's hash.
|
|
||||||
headHeaderKey = []byte("LastHeader")
|
|
||||||
|
|
||||||
// headBlockKey tracks the latest known full block's hash.
|
|
||||||
headBlockKey = []byte("LastBlock")
|
|
||||||
|
|
||||||
// headFastBlockKey tracks the latest known incomplete block's hash during fast sync.
|
|
||||||
headFastBlockKey = []byte("LastFast")
|
|
||||||
|
|
||||||
// headFinalizedBlockKey tracks the latest known finalized block hash.
|
|
||||||
headFinalizedBlockKey = []byte("LastFinalized")
|
|
||||||
|
|
||||||
// persistentStateIDKey tracks the id of latest stored state(for path-based only).
|
|
||||||
persistentStateIDKey = []byte("LastStateID")
|
|
||||||
|
|
||||||
// lastPivotKey tracks the last pivot block used by fast sync (to reenable on sethead).
|
|
||||||
lastPivotKey = []byte("LastPivot")
|
|
||||||
|
|
||||||
// fastTrieProgressKey tracks the number of trie entries imported during fast sync.
|
|
||||||
fastTrieProgressKey = []byte("TrieSync")
|
|
||||||
|
|
||||||
// snapshotDisabledKey flags that the snapshot should not be maintained due to initial sync.
|
|
||||||
snapshotDisabledKey = []byte("SnapshotDisabled")
|
|
||||||
|
|
||||||
// SnapshotRootKey tracks the hash of the last snapshot.
|
|
||||||
SnapshotRootKey = []byte("SnapshotRoot")
|
|
||||||
|
|
||||||
// snapshotJournalKey tracks the in-memory diff layers across restarts.
|
|
||||||
snapshotJournalKey = []byte("SnapshotJournal")
|
|
||||||
|
|
||||||
// snapshotGeneratorKey tracks the snapshot generation marker across restarts.
|
|
||||||
snapshotGeneratorKey = []byte("SnapshotGenerator")
|
|
||||||
|
|
||||||
// snapshotRecoveryKey tracks the snapshot recovery marker across restarts.
|
|
||||||
snapshotRecoveryKey = []byte("SnapshotRecovery")
|
|
||||||
|
|
||||||
// snapshotSyncStatusKey tracks the snapshot sync status across restarts.
|
|
||||||
snapshotSyncStatusKey = []byte("SnapshotSyncStatus")
|
|
||||||
|
|
||||||
// skeletonSyncStatusKey tracks the skeleton sync status across restarts.
|
|
||||||
skeletonSyncStatusKey = []byte("SkeletonSyncStatus")
|
|
||||||
|
|
||||||
// trieJournalKey tracks the in-memory trie node layers across restarts.
|
|
||||||
trieJournalKey = []byte("TrieJournal")
|
|
||||||
|
|
||||||
// txIndexTailKey tracks the oldest block whose transactions have been indexed.
|
|
||||||
txIndexTailKey = []byte("TransactionIndexTail")
|
|
||||||
|
|
||||||
// fastTxLookupLimitKey tracks the transaction lookup limit during fast sync.
|
|
||||||
fastTxLookupLimitKey = []byte("FastTransactionLookupLimit")
|
|
||||||
|
|
||||||
// badBlockKey tracks the list of bad blocks seen by local
|
|
||||||
badBlockKey = []byte("InvalidBlock")
|
|
||||||
|
|
||||||
// uncleanShutdownKey tracks the list of local crashes
|
|
||||||
uncleanShutdownKey = []byte("unclean-shutdown") // config prefix for the db
|
|
||||||
|
|
||||||
// transitionStatusKey tracks the eth2 transition status.
|
|
||||||
transitionStatusKey = []byte("eth2-transition")
|
|
||||||
|
|
||||||
// snapSyncStatusFlagKey flags that status of snap sync.
|
|
||||||
snapSyncStatusFlagKey = []byte("SnapSyncStatus")
|
|
||||||
|
|
||||||
// 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
|
|
||||||
headerHashSuffix = []byte("n") // headerPrefix + num (uint64 big endian) + headerHashSuffix -> hash
|
|
||||||
headerNumberPrefix = []byte("H") // headerNumberPrefix + hash -> num (uint64 big endian)
|
|
||||||
|
|
||||||
blockBodyPrefix = []byte("b") // blockBodyPrefix + num (uint64 big endian) + hash -> block body
|
|
||||||
blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts
|
|
||||||
|
|
||||||
txLookupPrefix = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata
|
|
||||||
bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits
|
|
||||||
SnapshotAccountPrefix = []byte("a") // SnapshotAccountPrefix + account hash -> account trie value
|
|
||||||
SnapshotStoragePrefix = []byte("o") // SnapshotStoragePrefix + account hash + storage hash -> storage trie value
|
|
||||||
CodePrefix = []byte("c") // CodePrefix + code hash -> account code
|
|
||||||
skeletonHeaderPrefix = []byte("S") // skeletonHeaderPrefix + num (uint64 big endian) -> header
|
|
||||||
|
|
||||||
// Path-based storage scheme of merkle patricia trie.
|
|
||||||
trieNodeAccountPrefix = []byte("A") // trieNodeAccountPrefix + hexPath -> trie node
|
|
||||||
trieNodeStoragePrefix = []byte("O") // trieNodeStoragePrefix + accountHash + hexPath -> trie node
|
|
||||||
stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id
|
|
||||||
|
|
||||||
PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage
|
|
||||||
configPrefix = []byte("ethereum-config-") // config prefix for the db
|
|
||||||
genesisPrefix = []byte("ethereum-genesis-") // genesis state prefix for the db
|
|
||||||
|
|
||||||
// BloomBitsIndexPrefix is the data table of a chain indexer to track its progress
|
|
||||||
BloomBitsIndexPrefix = []byte("iB")
|
|
||||||
|
|
||||||
ChtPrefix = []byte("chtRootV2-") // ChtPrefix + chtNum (uint64 big endian) -> trie root hash
|
|
||||||
ChtTablePrefix = []byte("cht-")
|
|
||||||
ChtIndexTablePrefix = []byte("chtIndexV2-")
|
|
||||||
|
|
||||||
BloomTriePrefix = []byte("bltRoot-") // BloomTriePrefix + bloomTrieNum (uint64 big endian) -> trie root hash
|
|
||||||
BloomTrieTablePrefix = []byte("blt-")
|
|
||||||
BloomTrieIndexPrefix = []byte("bltIndex-")
|
|
||||||
|
|
||||||
CliqueSnapshotPrefix = []byte("clique-")
|
|
||||||
|
|
||||||
BestUpdateKey = []byte("update-") // bigEndian64(syncPeriod) -> RLP(types.LightClientUpdate) (nextCommittee only referenced by root hash)
|
|
||||||
FixedCommitteeRootKey = []byte("fixedRoot-") // bigEndian64(syncPeriod) -> committee root hash
|
|
||||||
SyncCommitteeKey = []byte("committee-") // bigEndian64(syncPeriod) -> serialized committee
|
|
||||||
|
|
||||||
preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil)
|
|
||||||
preimageHitCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary
|
|
||||||
// fields.
|
|
||||||
type LegacyTxLookupEntry struct {
|
|
||||||
BlockHash common.Hash
|
|
||||||
BlockIndex uint64
|
|
||||||
Index uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeBlockNumber encodes a block number as big endian uint64
|
|
||||||
func encodeBlockNumber(number uint64) []byte {
|
|
||||||
enc := make([]byte, 8)
|
|
||||||
binary.BigEndian.PutUint64(enc, number)
|
|
||||||
return enc
|
|
||||||
}
|
|
||||||
|
|
||||||
// headerKeyPrefix = headerPrefix + num (uint64 big endian)
|
|
||||||
func headerKeyPrefix(number uint64) []byte {
|
|
||||||
return append(headerPrefix, encodeBlockNumber(number)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// headerKey = headerPrefix + num (uint64 big endian) + hash
|
|
||||||
func headerKey(number uint64, hash common.Hash) []byte {
|
|
||||||
return append(append(headerPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// headerTDKey = headerPrefix + num (uint64 big endian) + hash + headerTDSuffix
|
|
||||||
func headerTDKey(number uint64, hash common.Hash) []byte {
|
|
||||||
return append(headerKey(number, hash), headerTDSuffix...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// headerHashKey = headerPrefix + num (uint64 big endian) + headerHashSuffix
|
|
||||||
func headerHashKey(number uint64) []byte {
|
|
||||||
return append(append(headerPrefix, encodeBlockNumber(number)...), headerHashSuffix...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// headerNumberKey = headerNumberPrefix + hash
|
|
||||||
func headerNumberKey(hash common.Hash) []byte {
|
|
||||||
return append(headerNumberPrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockBodyKey = blockBodyPrefix + num (uint64 big endian) + hash
|
|
||||||
func blockBodyKey(number uint64, hash common.Hash) []byte {
|
|
||||||
return append(append(blockBodyPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockReceiptsKey = blockReceiptsPrefix + num (uint64 big endian) + hash
|
|
||||||
func blockReceiptsKey(number uint64, hash common.Hash) []byte {
|
|
||||||
return append(append(blockReceiptsPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// txLookupKey = txLookupPrefix + hash
|
|
||||||
func txLookupKey(hash common.Hash) []byte {
|
|
||||||
return append(txLookupPrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountSnapshotKey = SnapshotAccountPrefix + hash
|
|
||||||
func accountSnapshotKey(hash common.Hash) []byte {
|
|
||||||
return append(SnapshotAccountPrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// storageSnapshotKey = SnapshotStoragePrefix + account hash + storage hash
|
|
||||||
func storageSnapshotKey(accountHash, storageHash common.Hash) []byte {
|
|
||||||
buf := make([]byte, len(SnapshotStoragePrefix)+common.HashLength+common.HashLength)
|
|
||||||
n := copy(buf, SnapshotStoragePrefix)
|
|
||||||
n += copy(buf[n:], accountHash.Bytes())
|
|
||||||
copy(buf[n:], storageHash.Bytes())
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// storageSnapshotsKey = SnapshotStoragePrefix + account hash + storage hash
|
|
||||||
func storageSnapshotsKey(accountHash common.Hash) []byte {
|
|
||||||
return append(SnapshotStoragePrefix, accountHash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// bloomBitsKey = bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash
|
|
||||||
func bloomBitsKey(bit uint, section uint64, hash common.Hash) []byte {
|
|
||||||
key := append(append(bloomBitsPrefix, make([]byte, 10)...), hash.Bytes()...)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(key[1:], uint16(bit))
|
|
||||||
binary.BigEndian.PutUint64(key[3:], section)
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// skeletonHeaderKey = skeletonHeaderPrefix + num (uint64 big endian)
|
|
||||||
func skeletonHeaderKey(number uint64) []byte {
|
|
||||||
return append(skeletonHeaderPrefix, encodeBlockNumber(number)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// preimageKey = PreimagePrefix + hash
|
|
||||||
func preimageKey(hash common.Hash) []byte {
|
|
||||||
return append(PreimagePrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// codeKey = CodePrefix + hash
|
|
||||||
func codeKey(hash common.Hash) []byte {
|
|
||||||
return append(CodePrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCodeKey reports whether the given byte slice is the key of contract code,
|
|
||||||
// if so return the raw code hash as well.
|
|
||||||
func IsCodeKey(key []byte) (bool, []byte) {
|
|
||||||
if bytes.HasPrefix(key, CodePrefix) && len(key) == common.HashLength+len(CodePrefix) {
|
|
||||||
return true, key[len(CodePrefix):]
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// configKey = configPrefix + hash
|
|
||||||
func configKey(hash common.Hash) []byte {
|
|
||||||
return append(configPrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// genesisStateSpecKey = genesisPrefix + hash
|
|
||||||
func genesisStateSpecKey(hash common.Hash) []byte {
|
|
||||||
return append(genesisPrefix, hash.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stateIDKey = stateIDPrefix + root (32 bytes)
|
|
||||||
func stateIDKey(root common.Hash) []byte {
|
|
||||||
return append(stateIDPrefix, root.Bytes()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountTrieNodeKey = trieNodeAccountPrefix + nodePath.
|
|
||||||
func accountTrieNodeKey(path []byte) []byte {
|
|
||||||
return append(trieNodeAccountPrefix, path...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// storageTrieNodeKey = trieNodeStoragePrefix + accountHash + nodePath.
|
|
||||||
func storageTrieNodeKey(accountHash common.Hash, path []byte) []byte {
|
|
||||||
buf := make([]byte, len(trieNodeStoragePrefix)+common.HashLength+len(path))
|
|
||||||
n := copy(buf, trieNodeStoragePrefix)
|
|
||||||
n += copy(buf[n:], accountHash.Bytes())
|
|
||||||
copy(buf[n:], path)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLegacyTrieNode reports whether a provided database entry is a legacy trie
|
|
||||||
// node. The characteristics of legacy trie node are:
|
|
||||||
// - the key length is 32 bytes
|
|
||||||
// - the key is the hash of val
|
|
||||||
func IsLegacyTrieNode(key []byte, val []byte) bool {
|
|
||||||
if len(key) != common.HashLength {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bytes.Equal(key, crypto.Keccak256(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveAccountTrieNodeKey reports whether a provided database entry is an
|
|
||||||
// account trie node in path-based state scheme, and returns the resolved
|
|
||||||
// node path if so.
|
|
||||||
func ResolveAccountTrieNodeKey(key []byte) (bool, []byte) {
|
|
||||||
if !bytes.HasPrefix(key, trieNodeAccountPrefix) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
// The remaining key should only consist a hex node path
|
|
||||||
// whose length is in the range 0 to 64 (64 is excluded
|
|
||||||
// since leaves are always wrapped with shortNode).
|
|
||||||
if len(key) >= len(trieNodeAccountPrefix)+common.HashLength*2 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, key[len(trieNodeAccountPrefix):]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAccountTrieNode reports whether a provided database entry is an account
|
|
||||||
// trie node in path-based state scheme.
|
|
||||||
func IsAccountTrieNode(key []byte) bool {
|
|
||||||
ok, _ := ResolveAccountTrieNodeKey(key)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveStorageTrieNode reports whether a provided database entry is a storage
|
|
||||||
// trie node in path-based state scheme, and returns the resolved account hash
|
|
||||||
// and node path if so.
|
|
||||||
func ResolveStorageTrieNode(key []byte) (bool, common.Hash, []byte) {
|
|
||||||
if !bytes.HasPrefix(key, trieNodeStoragePrefix) {
|
|
||||||
return false, common.Hash{}, nil
|
|
||||||
}
|
|
||||||
// The remaining key consists of 2 parts:
|
|
||||||
// - 32 bytes account hash
|
|
||||||
// - hex node path whose length is in the range 0 to 64
|
|
||||||
if len(key) < len(trieNodeStoragePrefix)+common.HashLength {
|
|
||||||
return false, common.Hash{}, nil
|
|
||||||
}
|
|
||||||
if len(key) >= len(trieNodeStoragePrefix)+common.HashLength+common.HashLength*2 {
|
|
||||||
return false, common.Hash{}, nil
|
|
||||||
}
|
|
||||||
accountHash := common.BytesToHash(key[len(trieNodeStoragePrefix) : len(trieNodeStoragePrefix)+common.HashLength])
|
|
||||||
return true, accountHash, key[len(trieNodeStoragePrefix)+common.HashLength:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsStorageTrieNode reports whether a provided database entry is a storage
|
|
||||||
// trie node in path-based state scheme.
|
|
||||||
func IsStorageTrieNode(key []byte) bool {
|
|
||||||
ok, _, _ := ResolveStorageTrieNode(key)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
@ -1,307 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// table is a wrapper around a database that prefixes each key access with a pre-
|
|
||||||
// configured string.
|
|
||||||
type table struct {
|
|
||||||
db ethdb.Database
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTable returns a database object that prefixes all keys with a given string.
|
|
||||||
func NewTable(db ethdb.Database, prefix string) ethdb.Database {
|
|
||||||
return &table{
|
|
||||||
db: db,
|
|
||||||
prefix: prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close is a noop to implement the Database interface.
|
|
||||||
func (t *table) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has retrieves if a prefixed version of a key is present in the database.
|
|
||||||
func (t *table) Has(key []byte) (bool, error) {
|
|
||||||
return t.db.Has(append([]byte(t.prefix), key...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves the given prefixed key if it's present in the database.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientRange is a noop passthrough that just forwards the request to the underlying
|
|
||||||
// database.
|
|
||||||
func (t *table) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
|
|
||||||
return t.db.AncientRange(kind, start, count, maxBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ancients is a noop passthrough that just forwards the request to the underlying
|
|
||||||
// database.
|
|
||||||
func (t *table) Ancients() (uint64, error) {
|
|
||||||
return t.db.Ancients()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tail is a noop passthrough that just forwards the request to the underlying
|
|
||||||
// database.
|
|
||||||
func (t *table) Tail() (uint64, error) {
|
|
||||||
return t.db.Tail()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyAncients runs an ancient write operation on the underlying database.
|
|
||||||
func (t *table) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (int64, error) {
|
|
||||||
return t.db.ModifyAncients(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *table) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) {
|
|
||||||
return t.db.ReadAncients(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateHead is a noop passthrough that just forwards the request to the underlying
|
|
||||||
// database.
|
|
||||||
func (t *table) TruncateHead(items uint64) (uint64, error) {
|
|
||||||
return t.db.TruncateHead(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateTail is a noop passthrough that just forwards the request to the underlying
|
|
||||||
// database.
|
|
||||||
func (t *table) TruncateTail(items uint64) (uint64, error) {
|
|
||||||
return t.db.TruncateTail(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync is a noop passthrough that just forwards the request to the underlying
|
|
||||||
// database.
|
|
||||||
func (t *table) Sync() error {
|
|
||||||
return t.db.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MigrateTable processes the entries in a given table in sequence
|
|
||||||
// converting them to a new format if they're of an old format.
|
|
||||||
func (t *table) MigrateTable(kind string, convert convertLegacyFn) error {
|
|
||||||
return t.db.MigrateTable(kind, convert)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AncientDatadir returns the ancient datadir of the underlying database.
|
|
||||||
func (t *table) AncientDatadir() (string, error) {
|
|
||||||
return t.db.AncientDatadir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return t.db.Put(append([]byte(t.prefix), key...), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the given prefixed key from the database.
|
|
||||||
func (t *table) Delete(key []byte) error {
|
|
||||||
return t.db.Delete(append([]byte(t.prefix), key...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIterator creates a binary-alphabetical iterator over a subset
|
|
||||||
// of database content with a particular key prefix, starting at a particular
|
|
||||||
// initial key (or after, if it does not exist).
|
|
||||||
func (t *table) NewIterator(prefix []byte, start []byte) ethdb.Iterator {
|
|
||||||
innerPrefix := append([]byte(t.prefix), prefix...)
|
|
||||||
iter := t.db.NewIterator(innerPrefix, start)
|
|
||||||
return &tableIterator{
|
|
||||||
iter: iter,
|
|
||||||
prefix: t.prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat returns a particular internal stat of the database.
|
|
||||||
func (t *table) Stat(property string) (string, error) {
|
|
||||||
return t.db.Stat(property)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compact flattens the underlying data store for the given key range. In essence,
|
|
||||||
// deleted and overwritten versions are discarded, and the data is rearranged to
|
|
||||||
// reduce the cost of operations needed to access them.
|
|
||||||
//
|
|
||||||
// A nil start is treated as a key before all keys in the data store; a nil limit
|
|
||||||
// is treated as a key after all keys in the data store. If both is nil then it
|
|
||||||
// will compact entire data store.
|
|
||||||
func (t *table) Compact(start []byte, limit []byte) error {
|
|
||||||
// If no start was specified, use the table prefix as the first value
|
|
||||||
if start == nil {
|
|
||||||
start = []byte(t.prefix)
|
|
||||||
} else {
|
|
||||||
start = append([]byte(t.prefix), start...)
|
|
||||||
}
|
|
||||||
// If no limit was specified, use the first element not matching the prefix
|
|
||||||
// as the limit
|
|
||||||
if limit == nil {
|
|
||||||
limit = []byte(t.prefix)
|
|
||||||
for i := len(limit) - 1; i >= 0; i-- {
|
|
||||||
// Bump the current character, stopping if it doesn't overflow
|
|
||||||
limit[i]++
|
|
||||||
if limit[i] > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Character overflown, proceed to the next or nil if the last
|
|
||||||
if i == 0 {
|
|
||||||
limit = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
limit = append([]byte(t.prefix), limit...)
|
|
||||||
}
|
|
||||||
// Range correctly calculated based on table prefix, delegate down
|
|
||||||
return t.db.Compact(start, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBatch creates a write-only database that buffers changes to its host db
|
|
||||||
// until a final write is called, each operation prefixing all keys with the
|
|
||||||
// pre-configured string.
|
|
||||||
func (t *table) NewBatch() ethdb.Batch {
|
|
||||||
return &tableBatch{t.db.NewBatch(), t.prefix}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBatchWithSize creates a write-only database batch with pre-allocated buffer.
|
|
||||||
func (t *table) NewBatchWithSize(size int) ethdb.Batch {
|
|
||||||
return &tableBatch{t.db.NewBatchWithSize(size), t.prefix}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSnapshot creates a database snapshot based on the current state.
|
|
||||||
// The created snapshot will not be affected by all following mutations
|
|
||||||
// happened on the database.
|
|
||||||
func (t *table) NewSnapshot() (ethdb.Snapshot, error) {
|
|
||||||
return t.db.NewSnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
// tableBatch is a wrapper around a database batch that prefixes each key access
|
|
||||||
// with a pre-configured string.
|
|
||||||
type tableBatch struct {
|
|
||||||
batch ethdb.Batch
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put inserts the given value into the batch for later committing.
|
|
||||||
func (b *tableBatch) Put(key, value []byte) error {
|
|
||||||
return b.batch.Put(append([]byte(b.prefix), key...), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete inserts a key removal into the batch for later committing.
|
|
||||||
func (b *tableBatch) Delete(key []byte) error {
|
|
||||||
return b.batch.Delete(append([]byte(b.prefix), key...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValueSize retrieves the amount of data queued up for writing.
|
|
||||||
func (b *tableBatch) ValueSize() int {
|
|
||||||
return b.batch.ValueSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write flushes any accumulated data to disk.
|
|
||||||
func (b *tableBatch) Write() error {
|
|
||||||
return b.batch.Write()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset resets the batch for reuse.
|
|
||||||
func (b *tableBatch) Reset() {
|
|
||||||
b.batch.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
// tableReplayer is a wrapper around a batch replayer which truncates
|
|
||||||
// the added prefix.
|
|
||||||
type tableReplayer struct {
|
|
||||||
w ethdb.KeyValueWriter
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put implements the interface KeyValueWriter.
|
|
||||||
func (r *tableReplayer) Put(key []byte, value []byte) error {
|
|
||||||
trimmed := key[len(r.prefix):]
|
|
||||||
return r.w.Put(trimmed, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete implements the interface KeyValueWriter.
|
|
||||||
func (r *tableReplayer) Delete(key []byte) error {
|
|
||||||
trimmed := key[len(r.prefix):]
|
|
||||||
return r.w.Delete(trimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replay replays the batch contents.
|
|
||||||
func (b *tableBatch) Replay(w ethdb.KeyValueWriter) error {
|
|
||||||
return b.batch.Replay(&tableReplayer{w: w, prefix: b.prefix})
|
|
||||||
}
|
|
||||||
|
|
||||||
// tableIterator is a wrapper around a database iterator that prefixes each key access
|
|
||||||
// with a pre-configured string.
|
|
||||||
type tableIterator struct {
|
|
||||||
iter ethdb.Iterator
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next moves the iterator to the next key/value pair. It returns whether the
|
|
||||||
// iterator is exhausted.
|
|
||||||
func (iter *tableIterator) Next() bool {
|
|
||||||
return iter.iter.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns any accumulated error. Exhausting all the key/value pairs
|
|
||||||
// is not considered to be an error.
|
|
||||||
func (iter *tableIterator) Error() error {
|
|
||||||
return iter.iter.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key returns the key of the current key/value pair, or nil if done. The caller
|
|
||||||
// should not modify the contents of the returned slice, and its contents may
|
|
||||||
// change on the next call to Next.
|
|
||||||
func (iter *tableIterator) Key() []byte {
|
|
||||||
key := iter.iter.Key()
|
|
||||||
if key == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return key[len(iter.prefix):]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns the value of the current key/value pair, or nil if done. The
|
|
||||||
// caller should not modify the contents of the returned slice, and its contents
|
|
||||||
// may change on the next call to Next.
|
|
||||||
func (iter *tableIterator) Value() []byte {
|
|
||||||
return iter.iter.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release releases associated resources. Release should always succeed and can
|
|
||||||
// be called multiple times without causing error.
|
|
||||||
func (iter *tableIterator) Release() {
|
|
||||||
iter.iter.Release()
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rawdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTableDatabase(t *testing.T) { testTableDatabase(t, "prefix") }
|
|
||||||
func TestEmptyPrefixTableDatabase(t *testing.T) { testTableDatabase(t, "") }
|
|
||||||
|
|
||||||
type testReplayer struct {
|
|
||||||
puts [][]byte
|
|
||||||
dels [][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testReplayer) Put(key []byte, value []byte) error {
|
|
||||||
r.puts = append(r.puts, key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testReplayer) Delete(key []byte) error {
|
|
||||||
r.dels = append(r.dels, key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func testTableDatabase(t *testing.T, prefix string) {
|
|
||||||
db := NewTable(NewMemoryDatabase(), prefix)
|
|
||||||
|
|
||||||
var entries = []struct {
|
|
||||||
key []byte
|
|
||||||
value []byte
|
|
||||||
}{
|
|
||||||
{[]byte{0x01, 0x02}, []byte{0x0a, 0x0b}},
|
|
||||||
{[]byte{0x03, 0x04}, []byte{0x0c, 0x0d}},
|
|
||||||
{[]byte{0x05, 0x06}, []byte{0x0e, 0x0f}},
|
|
||||||
|
|
||||||
{[]byte{0xff, 0xff, 0x01}, []byte{0x1a, 0x1b}},
|
|
||||||
{[]byte{0xff, 0xff, 0x02}, []byte{0x1c, 0x1d}},
|
|
||||||
{[]byte{0xff, 0xff, 0x03}, []byte{0x1e, 0x1f}},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Put/Get operation
|
|
||||||
for _, entry := range entries {
|
|
||||||
db.Put(entry.key, entry.value)
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
got, err := db.Get(entry.key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get value: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, entry.value) {
|
|
||||||
t.Fatalf("Value mismatch: want=%v, got=%v", entry.value, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test batch operation
|
|
||||||
db = NewTable(NewMemoryDatabase(), prefix)
|
|
||||||
batch := db.NewBatch()
|
|
||||||
for _, entry := range entries {
|
|
||||||
batch.Put(entry.key, entry.value)
|
|
||||||
}
|
|
||||||
batch.Write()
|
|
||||||
for _, entry := range entries {
|
|
||||||
got, err := db.Get(entry.key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get value: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, entry.value) {
|
|
||||||
t.Fatalf("Value mismatch: want=%v, got=%v", entry.value, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test batch replayer
|
|
||||||
r := &testReplayer{}
|
|
||||||
batch.Replay(r)
|
|
||||||
for index, entry := range entries {
|
|
||||||
got := r.puts[index]
|
|
||||||
if !bytes.Equal(got, entry.key) {
|
|
||||||
t.Fatalf("Key mismatch: want=%v, got=%v", entry.key, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
check := func(iter ethdb.Iterator, expCount, index int) {
|
|
||||||
count := 0
|
|
||||||
for iter.Next() {
|
|
||||||
key, value := iter.Key(), iter.Value()
|
|
||||||
if !bytes.Equal(key, entries[index].key) {
|
|
||||||
t.Fatalf("Key mismatch: want=%v, got=%v", entries[index].key, key)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(value, entries[index].value) {
|
|
||||||
t.Fatalf("Value mismatch: want=%v, got=%v", entries[index].value, value)
|
|
||||||
}
|
|
||||||
index += 1
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
if count != expCount {
|
|
||||||
t.Fatalf("Wrong number of elems, exp %d got %d", expCount, count)
|
|
||||||
}
|
|
||||||
iter.Release()
|
|
||||||
}
|
|
||||||
// Test iterators
|
|
||||||
check(db.NewIterator(nil, nil), 6, 0)
|
|
||||||
// Test iterators with prefix
|
|
||||||
check(db.NewIterator([]byte{0xff, 0xff}, nil), 3, 3)
|
|
||||||
// Test iterators with start point
|
|
||||||
check(db.NewIterator(nil, []byte{0xff, 0xff, 0x02}), 2, 4)
|
|
||||||
// Test iterators with prefix and start point
|
|
||||||
check(db.NewIterator([]byte{0xee}, nil), 0, 0)
|
|
||||||
check(db.NewIterator(nil, []byte{0x00}), 6, 0)
|
|
||||||
}
|
|
||||||
BIN
core/rawdb/testdata/stored_receipts.bin
vendored
BIN
core/rawdb/testdata/stored_receipts.bin
vendored
Binary file not shown.
197
core/rlp_test.go
197
core/rlp_test.go
|
|
@ -1,197 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getBlock(transactions int, uncles int, dataSize int) *types.Block {
|
|
||||||
var (
|
|
||||||
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
|
|
||||||
engine = ethash.NewFaker()
|
|
||||||
|
|
||||||
// A sender who makes transactions, has some funds
|
|
||||||
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
address = crypto.PubkeyToAddress(key.PublicKey)
|
|
||||||
funds = big.NewInt(1_000_000_000_000_000_000)
|
|
||||||
gspec = &Genesis{
|
|
||||||
Config: params.TestChainConfig,
|
|
||||||
Alloc: GenesisAlloc{address: {Balance: funds}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// We need to generate as many blocks +1 as uncles
|
|
||||||
_, blocks, _ := GenerateChainWithGenesis(gspec, engine, uncles+1,
|
|
||||||
func(n int, b *BlockGen) {
|
|
||||||
if n == uncles {
|
|
||||||
// Add transactions and stuff on the last block
|
|
||||||
for i := 0; i < transactions; i++ {
|
|
||||||
tx, _ := types.SignTx(types.NewTransaction(uint64(i), aa,
|
|
||||||
big.NewInt(0), 50000, b.header.BaseFee, make([]byte, dataSize)), types.HomesteadSigner{}, key)
|
|
||||||
b.AddTx(tx)
|
|
||||||
}
|
|
||||||
for i := 0; i < uncles; i++ {
|
|
||||||
b.AddUncle(&types.Header{ParentHash: b.PrevBlock(n - 1 - i).Hash(), Number: big.NewInt(int64(n - i))})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
block := blocks[len(blocks)-1]
|
|
||||||
return block
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRlpIterator tests that individual transactions can be picked out
|
|
||||||
// from blocks without full unmarshalling/marshalling
|
|
||||||
func TestRlpIterator(t *testing.T) {
|
|
||||||
for _, tt := range []struct {
|
|
||||||
txs int
|
|
||||||
uncles int
|
|
||||||
datasize int
|
|
||||||
}{
|
|
||||||
{0, 0, 0},
|
|
||||||
{0, 2, 0},
|
|
||||||
{10, 0, 0},
|
|
||||||
{10, 2, 0},
|
|
||||||
{10, 2, 50},
|
|
||||||
} {
|
|
||||||
testRlpIterator(t, tt.txs, tt.uncles, tt.datasize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRlpIterator(t *testing.T, txs, uncles, datasize int) {
|
|
||||||
desc := fmt.Sprintf("%d txs [%d datasize] and %d uncles", txs, datasize, uncles)
|
|
||||||
bodyRlp, _ := rlp.EncodeToBytes(getBlock(txs, uncles, datasize).Body())
|
|
||||||
it, err := rlp.NewListIterator(bodyRlp)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Check that txs exist
|
|
||||||
if !it.Next() {
|
|
||||||
t.Fatal("expected two elems, got zero")
|
|
||||||
}
|
|
||||||
txdata := it.Value()
|
|
||||||
// Check that uncles exist
|
|
||||||
if !it.Next() {
|
|
||||||
t.Fatal("expected two elems, got one")
|
|
||||||
}
|
|
||||||
// No more after that
|
|
||||||
if it.Next() {
|
|
||||||
t.Fatal("expected only two elems, got more")
|
|
||||||
}
|
|
||||||
txIt, err := rlp.NewListIterator(txdata)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var gotHashes []common.Hash
|
|
||||||
var expHashes []common.Hash
|
|
||||||
for txIt.Next() {
|
|
||||||
gotHashes = append(gotHashes, crypto.Keccak256Hash(txIt.Value()))
|
|
||||||
}
|
|
||||||
|
|
||||||
var expBody types.Body
|
|
||||||
err = rlp.DecodeBytes(bodyRlp, &expBody)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, tx := range expBody.Transactions {
|
|
||||||
expHashes = append(expHashes, tx.Hash())
|
|
||||||
}
|
|
||||||
if gotLen, expLen := len(gotHashes), len(expHashes); gotLen != expLen {
|
|
||||||
t.Fatalf("testcase %v: length wrong, got %d exp %d", desc, gotLen, expLen)
|
|
||||||
}
|
|
||||||
// also sanity check against input
|
|
||||||
if gotLen := len(gotHashes); gotLen != txs {
|
|
||||||
t.Fatalf("testcase %v: length wrong, got %d exp %d", desc, gotLen, txs)
|
|
||||||
}
|
|
||||||
for i, got := range gotHashes {
|
|
||||||
if exp := expHashes[i]; got != exp {
|
|
||||||
t.Errorf("testcase %v: hash wrong, got %x, exp %x", desc, got, exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkHashing compares the speeds of hashing a rlp raw data directly
|
|
||||||
// without the unmarshalling/marshalling step
|
|
||||||
func BenchmarkHashing(b *testing.B) {
|
|
||||||
// Make a pretty fat block
|
|
||||||
var (
|
|
||||||
bodyRlp []byte
|
|
||||||
blockRlp []byte
|
|
||||||
)
|
|
||||||
{
|
|
||||||
block := getBlock(200, 2, 50)
|
|
||||||
bodyRlp, _ = rlp.EncodeToBytes(block.Body())
|
|
||||||
blockRlp, _ = rlp.EncodeToBytes(block)
|
|
||||||
}
|
|
||||||
var got common.Hash
|
|
||||||
var hasher = sha3.NewLegacyKeccak256()
|
|
||||||
b.Run("iteratorhashing", func(b *testing.B) {
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var hash common.Hash
|
|
||||||
it, err := rlp.NewListIterator(bodyRlp)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
it.Next()
|
|
||||||
txs := it.Value()
|
|
||||||
txIt, err := rlp.NewListIterator(txs)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
for txIt.Next() {
|
|
||||||
hasher.Reset()
|
|
||||||
hasher.Write(txIt.Value())
|
|
||||||
hasher.Sum(hash[:0])
|
|
||||||
got = hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
var exp common.Hash
|
|
||||||
b.Run("fullbodyhashing", func(b *testing.B) {
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var body types.Body
|
|
||||||
rlp.DecodeBytes(bodyRlp, &body)
|
|
||||||
for _, tx := range body.Transactions {
|
|
||||||
exp = tx.Hash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
b.Run("fullblockhashing", func(b *testing.B) {
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var block types.Block
|
|
||||||
rlp.DecodeBytes(blockRlp, &block)
|
|
||||||
for _, tx := range block.Transactions() {
|
|
||||||
tx.Hash()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if got != exp {
|
|
||||||
b.Fatalf("hash wrong, got %x exp %x", got, exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SenderCacher is a concurrent transaction sender recoverer and cacher.
|
|
||||||
var SenderCacher = newTxSenderCacher(runtime.NumCPU())
|
|
||||||
|
|
||||||
// txSenderCacherRequest is a request for recovering transaction senders with a
|
|
||||||
// specific signature scheme and caching it into the transactions themselves.
|
|
||||||
//
|
|
||||||
// The inc field defines the number of transactions to skip after each recovery,
|
|
||||||
// which is used to feed the same underlying input array to different threads but
|
|
||||||
// ensure they process the early transactions fast.
|
|
||||||
type txSenderCacherRequest struct {
|
|
||||||
signer types.Signer
|
|
||||||
txs []*types.Transaction
|
|
||||||
inc int
|
|
||||||
}
|
|
||||||
|
|
||||||
// txSenderCacher is a helper structure to concurrently ecrecover transaction
|
|
||||||
// senders from digital signatures on background threads.
|
|
||||||
type txSenderCacher struct {
|
|
||||||
threads int
|
|
||||||
tasks chan *txSenderCacherRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTxSenderCacher creates a new transaction sender background cacher and starts
|
|
||||||
// as many processing goroutines as allowed by the GOMAXPROCS on construction.
|
|
||||||
func newTxSenderCacher(threads int) *txSenderCacher {
|
|
||||||
cacher := &txSenderCacher{
|
|
||||||
tasks: make(chan *txSenderCacherRequest, threads),
|
|
||||||
threads: threads,
|
|
||||||
}
|
|
||||||
for i := 0; i < threads; i++ {
|
|
||||||
go cacher.cache()
|
|
||||||
}
|
|
||||||
return cacher
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache is an infinite loop, caching transaction senders from various forms of
|
|
||||||
// data structures.
|
|
||||||
func (cacher *txSenderCacher) cache() {
|
|
||||||
for task := range cacher.tasks {
|
|
||||||
for i := 0; i < len(task.txs); i += task.inc {
|
|
||||||
types.Sender(task.signer, task.txs[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recover recovers the senders from a batch of transactions and caches them
|
|
||||||
// back into the same data structures. There is no validation being done, nor
|
|
||||||
// any reaction to invalid signatures. That is up to calling code later.
|
|
||||||
func (cacher *txSenderCacher) Recover(signer types.Signer, txs []*types.Transaction) {
|
|
||||||
// If there's nothing to recover, abort
|
|
||||||
if len(txs) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Ensure we have meaningful task sizes and schedule the recoveries
|
|
||||||
tasks := cacher.threads
|
|
||||||
if len(txs) < tasks*4 {
|
|
||||||
tasks = (len(txs) + 3) / 4
|
|
||||||
}
|
|
||||||
for i := 0; i < tasks; i++ {
|
|
||||||
cacher.tasks <- &txSenderCacherRequest{
|
|
||||||
signer: signer,
|
|
||||||
txs: txs[i:],
|
|
||||||
inc: tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecoverFromBlocks recovers the senders from a batch of blocks and caches them
|
|
||||||
// back into the same data structures. There is no validation being done, nor
|
|
||||||
// any reaction to invalid signatures. That is up to calling code later.
|
|
||||||
func (cacher *txSenderCacher) RecoverFromBlocks(signer types.Signer, blocks []*types.Block) {
|
|
||||||
count := 0
|
|
||||||
for _, block := range blocks {
|
|
||||||
count += len(block.Transactions())
|
|
||||||
}
|
|
||||||
txs := make([]*types.Transaction, 0, count)
|
|
||||||
for _, block := range blocks {
|
|
||||||
txs = append(txs, block.Transactions()...)
|
|
||||||
}
|
|
||||||
cacher.Recover(signer, txs)
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type accessList struct {
|
|
||||||
addresses map[common.Address]int
|
|
||||||
slots []map[common.Hash]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainsAddress returns true if the address is in the access list.
|
|
||||||
func (al *accessList) ContainsAddress(address common.Address) bool {
|
|
||||||
_, ok := al.addresses[address]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains checks if a slot within an account is present in the access list, returning
|
|
||||||
// separate flags for the presence of the account and the slot respectively.
|
|
||||||
func (al *accessList) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) {
|
|
||||||
idx, ok := al.addresses[address]
|
|
||||||
if !ok {
|
|
||||||
// no such address (and hence zero slots)
|
|
||||||
return false, false
|
|
||||||
}
|
|
||||||
if idx == -1 {
|
|
||||||
// address yes, but no slots
|
|
||||||
return true, false
|
|
||||||
}
|
|
||||||
_, slotPresent = al.slots[idx][slot]
|
|
||||||
return true, slotPresent
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAccessList creates a new accessList.
|
|
||||||
func newAccessList() *accessList {
|
|
||||||
return &accessList{
|
|
||||||
addresses: make(map[common.Address]int),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy creates an independent copy of an accessList.
|
|
||||||
func (a *accessList) Copy() *accessList {
|
|
||||||
cp := newAccessList()
|
|
||||||
for k, v := range a.addresses {
|
|
||||||
cp.addresses[k] = v
|
|
||||||
}
|
|
||||||
cp.slots = make([]map[common.Hash]struct{}, len(a.slots))
|
|
||||||
for i, slotMap := range a.slots {
|
|
||||||
newSlotmap := make(map[common.Hash]struct{}, len(slotMap))
|
|
||||||
for k := range slotMap {
|
|
||||||
newSlotmap[k] = struct{}{}
|
|
||||||
}
|
|
||||||
cp.slots[i] = newSlotmap
|
|
||||||
}
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAddress adds an address to the access list, and returns 'true' if the operation
|
|
||||||
// caused a change (addr was not previously in the list).
|
|
||||||
func (al *accessList) AddAddress(address common.Address) bool {
|
|
||||||
if _, present := al.addresses[address]; present {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
al.addresses[address] = -1
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSlot adds the specified (addr, slot) combo to the access list.
|
|
||||||
// Return values are:
|
|
||||||
// - address added
|
|
||||||
// - slot added
|
|
||||||
// For any 'true' value returned, a corresponding journal entry must be made.
|
|
||||||
func (al *accessList) AddSlot(address common.Address, slot common.Hash) (addrChange bool, slotChange bool) {
|
|
||||||
idx, addrPresent := al.addresses[address]
|
|
||||||
if !addrPresent || idx == -1 {
|
|
||||||
// Address not present, or addr present but no slots there
|
|
||||||
al.addresses[address] = len(al.slots)
|
|
||||||
slotmap := map[common.Hash]struct{}{slot: {}}
|
|
||||||
al.slots = append(al.slots, slotmap)
|
|
||||||
return !addrPresent, true
|
|
||||||
}
|
|
||||||
// There is already an (address,slot) mapping
|
|
||||||
slotmap := al.slots[idx]
|
|
||||||
if _, ok := slotmap[slot]; !ok {
|
|
||||||
slotmap[slot] = struct{}{}
|
|
||||||
// Journal add slot change
|
|
||||||
return false, true
|
|
||||||
}
|
|
||||||
// No changes required
|
|
||||||
return false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSlot removes an (address, slot)-tuple from the access list.
|
|
||||||
// This operation needs to be performed in the same order as the addition happened.
|
|
||||||
// This method is meant to be used by the journal, which maintains ordering of
|
|
||||||
// operations.
|
|
||||||
func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) {
|
|
||||||
idx, addrOk := al.addresses[address]
|
|
||||||
// There are two ways this can fail
|
|
||||||
if !addrOk {
|
|
||||||
panic("reverting slot change, address not present in list")
|
|
||||||
}
|
|
||||||
slotmap := al.slots[idx]
|
|
||||||
delete(slotmap, slot)
|
|
||||||
// If that was the last (first) slot, remove it
|
|
||||||
// Since additions and rollbacks are always performed in order,
|
|
||||||
// we can delete the item without worrying about screwing up later indices
|
|
||||||
if len(slotmap) == 0 {
|
|
||||||
al.slots = al.slots[:idx]
|
|
||||||
al.addresses[address] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAddress removes an address from the access list. This operation
|
|
||||||
// needs to be performed in the same order as the addition happened.
|
|
||||||
// This method is meant to be used by the journal, which maintains ordering of
|
|
||||||
// operations.
|
|
||||||
func (al *accessList) DeleteAddress(address common.Address) {
|
|
||||||
delete(al.addresses, address)
|
|
||||||
}
|
|
||||||
|
|
@ -1,265 +0,0 @@
|
||||||
// Copyright 2017 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/crate-crypto/go-ipa/banderwagon"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/lru"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Number of codehash->size associations to keep.
|
|
||||||
codeSizeCacheSize = 100000
|
|
||||||
|
|
||||||
// Cache size granted for caching clean code.
|
|
||||||
codeCacheSize = 64 * 1024 * 1024
|
|
||||||
|
|
||||||
// commitmentSize is the size of commitment stored in cache.
|
|
||||||
commitmentSize = banderwagon.UncompressedSize
|
|
||||||
|
|
||||||
// Cache item granted for caching commitment results.
|
|
||||||
commitmentCacheItems = 64 * 1024 * 1024 / (commitmentSize + common.AddressLength)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database wraps access to tries and contract code.
|
|
||||||
type Database interface {
|
|
||||||
// OpenTrie opens the main account trie.
|
|
||||||
OpenTrie(root common.Hash) (Trie, error)
|
|
||||||
|
|
||||||
// OpenStorageTrie opens the storage trie of an account.
|
|
||||||
OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, trie Trie) (Trie, error)
|
|
||||||
|
|
||||||
// CopyTrie returns an independent copy of the given trie.
|
|
||||||
CopyTrie(Trie) Trie
|
|
||||||
|
|
||||||
// ContractCode retrieves a particular contract's code.
|
|
||||||
ContractCode(addr common.Address, codeHash common.Hash) ([]byte, error)
|
|
||||||
|
|
||||||
// ContractCodeSize retrieves a particular contracts code's size.
|
|
||||||
ContractCodeSize(addr common.Address, codeHash common.Hash) (int, error)
|
|
||||||
|
|
||||||
// DiskDB returns the underlying key-value disk database.
|
|
||||||
DiskDB() ethdb.KeyValueStore
|
|
||||||
|
|
||||||
// TrieDB returns the underlying trie database for managing trie nodes.
|
|
||||||
TrieDB() *trie.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trie is a Ethereum Merkle Patricia trie.
|
|
||||||
type Trie interface {
|
|
||||||
// GetKey returns the sha3 preimage of a hashed key that was previously used
|
|
||||||
// to store a value.
|
|
||||||
//
|
|
||||||
// TODO(fjl): remove this when StateTrie is removed
|
|
||||||
GetKey([]byte) []byte
|
|
||||||
|
|
||||||
// GetAccount abstracts an account read from the trie. It retrieves the
|
|
||||||
// account blob from the trie with provided account address and decodes it
|
|
||||||
// with associated decoding algorithm. If the specified account is not in
|
|
||||||
// the trie, nil will be returned. If the trie is corrupted(e.g. some nodes
|
|
||||||
// are missing or the account blob is incorrect for decoding), an error will
|
|
||||||
// be returned.
|
|
||||||
GetAccount(address common.Address) (*types.StateAccount, error)
|
|
||||||
|
|
||||||
// GetStorage returns the value for key stored in the trie. The value bytes
|
|
||||||
// must not be modified by the caller. If a node was not found in the database,
|
|
||||||
// a trie.MissingNodeError is returned.
|
|
||||||
GetStorage(addr common.Address, key []byte) ([]byte, error)
|
|
||||||
|
|
||||||
// UpdateAccount abstracts an account write to the trie. It encodes the
|
|
||||||
// provided account object with associated algorithm and then updates it
|
|
||||||
// in the trie with provided address.
|
|
||||||
UpdateAccount(address common.Address, account *types.StateAccount) error
|
|
||||||
|
|
||||||
// UpdateStorage associates key with value in the trie. If value has length zero,
|
|
||||||
// any existing value is deleted from the trie. The value bytes must not be modified
|
|
||||||
// by the caller while they are stored in the trie. If a node was not found in the
|
|
||||||
// database, a trie.MissingNodeError is returned.
|
|
||||||
UpdateStorage(addr common.Address, key, value []byte) error
|
|
||||||
|
|
||||||
// DeleteAccount abstracts an account deletion from the trie.
|
|
||||||
DeleteAccount(address common.Address) error
|
|
||||||
|
|
||||||
// DeleteStorage removes any existing value for key from the trie. If a node
|
|
||||||
// was not found in the database, a trie.MissingNodeError is returned.
|
|
||||||
DeleteStorage(addr common.Address, key []byte) error
|
|
||||||
|
|
||||||
// UpdateContractCode abstracts code write to the trie. It is expected
|
|
||||||
// to be moved to the stateWriter interface when the latter is ready.
|
|
||||||
UpdateContractCode(address common.Address, codeHash common.Hash, code []byte) error
|
|
||||||
|
|
||||||
// Hash returns the root hash of the trie. It does not write to the database and
|
|
||||||
// can be used even if the trie doesn't have one.
|
|
||||||
Hash() common.Hash
|
|
||||||
|
|
||||||
// Commit collects all dirty nodes in the trie and replace them with the
|
|
||||||
// corresponding node hash. All collected nodes(including dirty leaves if
|
|
||||||
// collectLeaf is true) will be encapsulated into a nodeset for return.
|
|
||||||
// The returned nodeset can be nil if the trie is clean(nothing to commit).
|
|
||||||
// Once the trie is committed, it's not usable anymore. A new trie must
|
|
||||||
// be created with new root and updated trie database for following usage
|
|
||||||
Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error)
|
|
||||||
|
|
||||||
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
|
|
||||||
// starts at the key after the given start key. And error will be returned
|
|
||||||
// if fails to create node iterator.
|
|
||||||
NodeIterator(startKey []byte) (trie.NodeIterator, error)
|
|
||||||
|
|
||||||
// Prove constructs a Merkle proof for key. The result contains all encoded nodes
|
|
||||||
// on the path to the value at key. The value itself is also included in the last
|
|
||||||
// node and can be retrieved by verifying the proof.
|
|
||||||
//
|
|
||||||
// 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, proofDb ethdb.KeyValueWriter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabase creates a backing store for state. The returned database is safe for
|
|
||||||
// concurrent use, but does not retain any recent trie nodes in memory. To keep some
|
|
||||||
// historical state in memory, use the NewDatabaseWithConfig constructor.
|
|
||||||
func NewDatabase(db ethdb.Database) Database {
|
|
||||||
return NewDatabaseWithConfig(db, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabaseWithConfig creates a backing store for state. The returned database
|
|
||||||
// is safe for concurrent use and retains a lot of collapsed RLP trie nodes in a
|
|
||||||
// large memory cache.
|
|
||||||
func NewDatabaseWithConfig(db ethdb.Database, config *trie.Config) Database {
|
|
||||||
return &cachingDB{
|
|
||||||
disk: db,
|
|
||||||
codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize),
|
|
||||||
codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize),
|
|
||||||
triedb: trie.NewDatabase(db, config),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabaseWithNodeDB creates a state database with an already initialized node database.
|
|
||||||
func NewDatabaseWithNodeDB(db ethdb.Database, triedb *trie.Database) Database {
|
|
||||||
return &cachingDB{
|
|
||||||
disk: db,
|
|
||||||
codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize),
|
|
||||||
codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize),
|
|
||||||
triedb: triedb,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type cachingDB struct {
|
|
||||||
disk ethdb.KeyValueStore
|
|
||||||
codeSizeCache *lru.Cache[common.Hash, int]
|
|
||||||
codeCache *lru.SizeConstrainedCache[common.Hash, []byte]
|
|
||||||
triedb *trie.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenTrie opens the main account trie at a specific root hash.
|
|
||||||
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
|
|
||||||
if db.triedb.IsVerkle() {
|
|
||||||
return trie.NewVerkleTrie(root, db.triedb, utils.NewPointCache(commitmentCacheItems))
|
|
||||||
}
|
|
||||||
tr, err := trie.NewStateTrie(trie.StateTrieID(root), db.triedb)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenStorageTrie opens the storage trie of an account.
|
|
||||||
func (db *cachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) {
|
|
||||||
// In the verkle case, there is only one tree. But the two-tree structure
|
|
||||||
// is hardcoded in the codebase. So we need to return the same trie in this
|
|
||||||
// case.
|
|
||||||
if db.triedb.IsVerkle() {
|
|
||||||
return self, nil
|
|
||||||
}
|
|
||||||
tr, err := trie.NewStateTrie(trie.StorageTrieID(stateRoot, crypto.Keccak256Hash(address.Bytes()), root), db.triedb)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyTrie returns an independent copy of the given trie.
|
|
||||||
func (db *cachingDB) CopyTrie(t Trie) Trie {
|
|
||||||
switch t := t.(type) {
|
|
||||||
case *trie.StateTrie:
|
|
||||||
return t.Copy()
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unknown trie type %T", t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContractCode retrieves a particular contract's code.
|
|
||||||
func (db *cachingDB) ContractCode(address common.Address, codeHash common.Hash) ([]byte, error) {
|
|
||||||
code, _ := db.codeCache.Get(codeHash)
|
|
||||||
if len(code) > 0 {
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
code = rawdb.ReadCode(db.disk, codeHash)
|
|
||||||
if len(code) > 0 {
|
|
||||||
db.codeCache.Add(codeHash, code)
|
|
||||||
db.codeSizeCache.Add(codeHash, len(code))
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContractCodeWithPrefix retrieves a particular contract's code. If the
|
|
||||||
// code can't be found in the cache, then check the existence with **new**
|
|
||||||
// db scheme.
|
|
||||||
func (db *cachingDB) ContractCodeWithPrefix(address common.Address, codeHash common.Hash) ([]byte, error) {
|
|
||||||
code, _ := db.codeCache.Get(codeHash)
|
|
||||||
if len(code) > 0 {
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
code = rawdb.ReadCodeWithPrefix(db.disk, codeHash)
|
|
||||||
if len(code) > 0 {
|
|
||||||
db.codeCache.Add(codeHash, code)
|
|
||||||
db.codeSizeCache.Add(codeHash, len(code))
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContractCodeSize retrieves a particular contracts code's size.
|
|
||||||
func (db *cachingDB) ContractCodeSize(addr common.Address, codeHash common.Hash) (int, error) {
|
|
||||||
if cached, ok := db.codeSizeCache.Get(codeHash); ok {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
code, err := db.ContractCode(addr, codeHash)
|
|
||||||
return len(code), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiskDB returns the underlying key-value disk database.
|
|
||||||
func (db *cachingDB) DiskDB() ethdb.KeyValueStore {
|
|
||||||
return db.disk
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrieDB retrieves any intermediate trie-node caching layer.
|
|
||||||
func (db *cachingDB) TrieDB() *trie.Database {
|
|
||||||
return db.triedb
|
|
||||||
}
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
// Copyright 2014 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DumpConfig is a set of options to control what portions of the state will be
|
|
||||||
// iterated and collected.
|
|
||||||
type DumpConfig struct {
|
|
||||||
SkipCode bool
|
|
||||||
SkipStorage bool
|
|
||||||
OnlyWithAddresses bool
|
|
||||||
Start []byte
|
|
||||||
Max uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// DumpCollector interface which the state trie calls during iteration
|
|
||||||
type DumpCollector interface {
|
|
||||||
// OnRoot is called with the state root
|
|
||||||
OnRoot(common.Hash)
|
|
||||||
// OnAccount is called once for each account in the trie
|
|
||||||
OnAccount(*common.Address, DumpAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DumpAccount represents an account in the state.
|
|
||||||
type DumpAccount struct {
|
|
||||||
Balance string `json:"balance"`
|
|
||||||
Nonce uint64 `json:"nonce"`
|
|
||||||
Root hexutil.Bytes `json:"root"`
|
|
||||||
CodeHash hexutil.Bytes `json:"codeHash"`
|
|
||||||
Code hexutil.Bytes `json:"code,omitempty"`
|
|
||||||
Storage map[common.Hash]string `json:"storage,omitempty"`
|
|
||||||
Address *common.Address `json:"address,omitempty"` // Address only present in iterative (line-by-line) mode
|
|
||||||
AddressHash hexutil.Bytes `json:"key,omitempty"` // If we don't have address, we can output the key
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dump represents the full dump in a collected format, as one large map.
|
|
||||||
type Dump struct {
|
|
||||||
Root string `json:"root"`
|
|
||||||
Accounts map[string]DumpAccount `json:"accounts"`
|
|
||||||
// Next can be set to represent that this dump is only partial, and Next
|
|
||||||
// is where an iterator should be positioned in order to continue the dump.
|
|
||||||
Next []byte `json:"next,omitempty"` // nil if no more accounts
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnRoot implements DumpCollector interface
|
|
||||||
func (d *Dump) OnRoot(root common.Hash) {
|
|
||||||
d.Root = fmt.Sprintf("%x", root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnAccount implements DumpCollector interface
|
|
||||||
func (d *Dump) OnAccount(addr *common.Address, account DumpAccount) {
|
|
||||||
if addr == nil {
|
|
||||||
d.Accounts[fmt.Sprintf("pre(%s)", account.AddressHash)] = account
|
|
||||||
}
|
|
||||||
if addr != nil {
|
|
||||||
d.Accounts[(*addr).String()] = account
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterativeDump is a DumpCollector-implementation which dumps output line-by-line iteratively.
|
|
||||||
type iterativeDump struct {
|
|
||||||
*json.Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnAccount implements DumpCollector interface
|
|
||||||
func (d iterativeDump) OnAccount(addr *common.Address, account DumpAccount) {
|
|
||||||
dumpAccount := &DumpAccount{
|
|
||||||
Balance: account.Balance,
|
|
||||||
Nonce: account.Nonce,
|
|
||||||
Root: account.Root,
|
|
||||||
CodeHash: account.CodeHash,
|
|
||||||
Code: account.Code,
|
|
||||||
Storage: account.Storage,
|
|
||||||
AddressHash: account.AddressHash,
|
|
||||||
Address: addr,
|
|
||||||
}
|
|
||||||
d.Encode(dumpAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnRoot implements DumpCollector interface
|
|
||||||
func (d iterativeDump) OnRoot(root common.Hash) {
|
|
||||||
d.Encode(struct {
|
|
||||||
Root common.Hash `json:"root"`
|
|
||||||
}{root})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DumpToCollector iterates the state according to the given options and inserts
|
|
||||||
// the items into a collector for aggregation or serialization.
|
|
||||||
func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) {
|
|
||||||
// Sanitize the input to allow nil configs
|
|
||||||
if conf == nil {
|
|
||||||
conf = new(DumpConfig)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
missingPreimages int
|
|
||||||
accounts uint64
|
|
||||||
start = time.Now()
|
|
||||||
logged = time.Now()
|
|
||||||
)
|
|
||||||
log.Info("Trie dumping started", "root", s.trie.Hash())
|
|
||||||
c.OnRoot(s.trie.Hash())
|
|
||||||
|
|
||||||
trieIt, err := s.trie.NodeIterator(conf.Start)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Trie dumping error", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
it := trie.NewIterator(trieIt)
|
|
||||||
for it.Next() {
|
|
||||||
var data types.StateAccount
|
|
||||||
if err := rlp.DecodeBytes(it.Value, &data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
account = DumpAccount{
|
|
||||||
Balance: data.Balance.String(),
|
|
||||||
Nonce: data.Nonce,
|
|
||||||
Root: data.Root[:],
|
|
||||||
CodeHash: data.CodeHash,
|
|
||||||
AddressHash: it.Key,
|
|
||||||
}
|
|
||||||
address *common.Address
|
|
||||||
addr common.Address
|
|
||||||
addrBytes = s.trie.GetKey(it.Key)
|
|
||||||
)
|
|
||||||
if addrBytes == nil {
|
|
||||||
missingPreimages++
|
|
||||||
if conf.OnlyWithAddresses {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addr = common.BytesToAddress(addrBytes)
|
|
||||||
address = &addr
|
|
||||||
account.Address = address
|
|
||||||
}
|
|
||||||
obj := newObject(s, addr, &data)
|
|
||||||
if !conf.SkipCode {
|
|
||||||
account.Code = obj.Code()
|
|
||||||
}
|
|
||||||
if !conf.SkipStorage {
|
|
||||||
account.Storage = make(map[common.Hash]string)
|
|
||||||
tr, err := obj.getTrie()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to load storage trie", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trieIt, err := tr.NodeIterator(nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to create trie iterator", "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
storageIt := trie.NewIterator(trieIt)
|
|
||||||
for storageIt.Next() {
|
|
||||||
_, content, _, err := rlp.Split(storageIt.Value)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to decode the value returned by iterator", "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
account.Storage[common.BytesToHash(s.trie.GetKey(storageIt.Key))] = common.Bytes2Hex(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.OnAccount(address, account)
|
|
||||||
accounts++
|
|
||||||
if time.Since(logged) > 8*time.Second {
|
|
||||||
log.Info("Trie dumping in progress", "at", it.Key, "accounts", accounts,
|
|
||||||
"elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
if conf.Max > 0 && accounts >= conf.Max {
|
|
||||||
if it.Next() {
|
|
||||||
nextKey = it.Key
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if missingPreimages > 0 {
|
|
||||||
log.Warn("Dump incomplete due to missing preimages", "missing", missingPreimages)
|
|
||||||
}
|
|
||||||
log.Info("Trie dumping complete", "accounts", accounts,
|
|
||||||
"elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
|
|
||||||
return nextKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawDump returns the state. If the processing is aborted e.g. due to options
|
|
||||||
// reaching Max, the `Next` key is set on the returned Dump.
|
|
||||||
func (s *StateDB) RawDump(opts *DumpConfig) Dump {
|
|
||||||
dump := &Dump{
|
|
||||||
Accounts: make(map[string]DumpAccount),
|
|
||||||
}
|
|
||||||
dump.Next = s.DumpToCollector(dump, opts)
|
|
||||||
return *dump
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dump returns a JSON string representing the entire state as a single json-object
|
|
||||||
func (s *StateDB) Dump(opts *DumpConfig) []byte {
|
|
||||||
dump := s.RawDump(opts)
|
|
||||||
json, err := json.MarshalIndent(dump, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error dumping state", "err", err)
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
|
|
||||||
// IterativeDump dumps out accounts as json-objects, delimited by linebreaks on stdout
|
|
||||||
func (s *StateDB) IterativeDump(opts *DumpConfig, output *json.Encoder) {
|
|
||||||
s.DumpToCollector(iterativeDump{output}, opts)
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
// Copyright 2015 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nodeIterator is an iterator to traverse the entire state trie post-order,
|
|
||||||
// including all of the contract code and contract state tries. Preimage is
|
|
||||||
// required in order to resolve the contract address.
|
|
||||||
type nodeIterator struct {
|
|
||||||
state *StateDB // State being iterated
|
|
||||||
|
|
||||||
stateIt trie.NodeIterator // Primary iterator for the global state trie
|
|
||||||
dataIt trie.NodeIterator // Secondary iterator for the data trie of a contract
|
|
||||||
|
|
||||||
accountHash common.Hash // Hash of the node containing the account
|
|
||||||
codeHash common.Hash // Hash of the contract source code
|
|
||||||
code []byte // Source code associated with a contract
|
|
||||||
|
|
||||||
Hash common.Hash // Hash of the current entry being iterated (nil if not standalone)
|
|
||||||
Parent common.Hash // Hash of the first full ancestor node (nil if current is the root)
|
|
||||||
|
|
||||||
Error error // Failure set in case of an internal error in the iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
// newNodeIterator creates an post-order state node iterator.
|
|
||||||
func newNodeIterator(state *StateDB) *nodeIterator {
|
|
||||||
return &nodeIterator{
|
|
||||||
state: state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next moves the iterator to the next node, returning whether there are any
|
|
||||||
// further nodes. In case of an internal error this method returns false and
|
|
||||||
// sets the Error field to the encountered failure.
|
|
||||||
func (it *nodeIterator) Next() bool {
|
|
||||||
// If the iterator failed previously, don't do anything
|
|
||||||
if it.Error != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Otherwise step forward with the iterator and report any errors
|
|
||||||
if err := it.step(); err != nil {
|
|
||||||
it.Error = err
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return it.retrieve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// step moves the iterator to the next entry of the state trie.
|
|
||||||
func (it *nodeIterator) step() error {
|
|
||||||
// Abort if we reached the end of the iteration
|
|
||||||
if it.state == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Initialize the iterator if we've just started
|
|
||||||
var err error
|
|
||||||
if it.stateIt == nil {
|
|
||||||
it.stateIt, err = it.state.trie.NodeIterator(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we had data nodes previously, we surely have at least state nodes
|
|
||||||
if it.dataIt != nil {
|
|
||||||
if cont := it.dataIt.Next(true); !cont {
|
|
||||||
if it.dataIt.Error() != nil {
|
|
||||||
return it.dataIt.Error()
|
|
||||||
}
|
|
||||||
it.dataIt = nil
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If we had source code previously, discard that
|
|
||||||
if it.code != nil {
|
|
||||||
it.code = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Step to the next state trie node, terminating if we're out of nodes
|
|
||||||
if cont := it.stateIt.Next(true); !cont {
|
|
||||||
if it.stateIt.Error() != nil {
|
|
||||||
return it.stateIt.Error()
|
|
||||||
}
|
|
||||||
it.state, it.stateIt = nil, nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If the state trie node is an internal entry, leave as is
|
|
||||||
if !it.stateIt.Leaf() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Otherwise we've reached an account node, initiate data iteration
|
|
||||||
var account types.StateAccount
|
|
||||||
if err := rlp.DecodeBytes(it.stateIt.LeafBlob(), &account); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Lookup the preimage of account hash
|
|
||||||
preimage := it.state.trie.GetKey(it.stateIt.LeafKey())
|
|
||||||
if preimage == nil {
|
|
||||||
return errors.New("account address is not available")
|
|
||||||
}
|
|
||||||
address := common.BytesToAddress(preimage)
|
|
||||||
|
|
||||||
// Traverse the storage slots belong to the account
|
|
||||||
dataTrie, err := it.state.db.OpenStorageTrie(it.state.originalRoot, address, account.Root, it.state.trie)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
it.dataIt, err = dataTrie.NodeIterator(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !it.dataIt.Next(true) {
|
|
||||||
it.dataIt = nil
|
|
||||||
}
|
|
||||||
if !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) {
|
|
||||||
it.codeHash = common.BytesToHash(account.CodeHash)
|
|
||||||
it.code, err = it.state.db.ContractCode(address, common.BytesToHash(account.CodeHash))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("code %x: %v", account.CodeHash, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.accountHash = it.stateIt.Parent()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve pulls and caches the current state entry the iterator is traversing.
|
|
||||||
// The method returns whether there are any more data left for inspection.
|
|
||||||
func (it *nodeIterator) retrieve() bool {
|
|
||||||
// Clear out any previously set values
|
|
||||||
it.Hash = common.Hash{}
|
|
||||||
|
|
||||||
// If the iteration's done, return no available data
|
|
||||||
if it.state == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Otherwise retrieve the current entry
|
|
||||||
switch {
|
|
||||||
case it.dataIt != nil:
|
|
||||||
it.Hash, it.Parent = it.dataIt.Hash(), it.dataIt.Parent()
|
|
||||||
if it.Parent == (common.Hash{}) {
|
|
||||||
it.Parent = it.accountHash
|
|
||||||
}
|
|
||||||
case it.code != nil:
|
|
||||||
it.Hash, it.Parent = it.codeHash, it.accountHash
|
|
||||||
case it.stateIt != nil:
|
|
||||||
it.Hash, it.Parent = it.stateIt.Hash(), it.stateIt.Parent()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
// Copyright 2016 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests that the node iterator indeed walks over the entire database contents.
|
|
||||||
func TestNodeIteratorCoverage(t *testing.T) {
|
|
||||||
testNodeIteratorCoverage(t, rawdb.HashScheme)
|
|
||||||
testNodeIteratorCoverage(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNodeIteratorCoverage(t *testing.T, scheme string) {
|
|
||||||
// Create some arbitrary test state to iterate
|
|
||||||
db, sdb, ndb, root, _ := makeTestState(scheme)
|
|
||||||
ndb.Commit(root, false)
|
|
||||||
|
|
||||||
state, err := New(root, sdb, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create state trie at %x: %v", root, err)
|
|
||||||
}
|
|
||||||
// Gather all the node hashes found by the iterator
|
|
||||||
hashes := make(map[common.Hash]struct{})
|
|
||||||
for it := newNodeIterator(state); it.Next(); {
|
|
||||||
if it.Hash != (common.Hash{}) {
|
|
||||||
hashes[it.Hash] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check in-disk nodes
|
|
||||||
var (
|
|
||||||
seenNodes = make(map[common.Hash]struct{})
|
|
||||||
seenCodes = make(map[common.Hash]struct{})
|
|
||||||
)
|
|
||||||
it := db.NewIterator(nil, nil)
|
|
||||||
for it.Next() {
|
|
||||||
ok, hash := isTrieNode(scheme, it.Key(), it.Value())
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seenNodes[hash] = struct{}{}
|
|
||||||
}
|
|
||||||
it.Release()
|
|
||||||
|
|
||||||
// Check in-disk codes
|
|
||||||
it = db.NewIterator(nil, nil)
|
|
||||||
for it.Next() {
|
|
||||||
ok, hash := rawdb.IsCodeKey(it.Key())
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := hashes[common.BytesToHash(hash)]; !ok {
|
|
||||||
t.Errorf("state entry not reported %x", it.Key())
|
|
||||||
}
|
|
||||||
seenCodes[common.BytesToHash(hash)] = struct{}{}
|
|
||||||
}
|
|
||||||
it.Release()
|
|
||||||
|
|
||||||
// Cross check the iterated hashes and the database/nodepool content
|
|
||||||
for hash := range hashes {
|
|
||||||
_, ok := seenNodes[hash]
|
|
||||||
if !ok {
|
|
||||||
_, ok = seenCodes[hash]
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("failed to retrieve reported node %x", hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isTrieNode is a helper function which reports if the provided
|
|
||||||
// database entry belongs to a trie node or not.
|
|
||||||
func isTrieNode(scheme string, key, val []byte) (bool, common.Hash) {
|
|
||||||
if scheme == rawdb.HashScheme {
|
|
||||||
if rawdb.IsLegacyTrieNode(key, val) {
|
|
||||||
return true, common.BytesToHash(key)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ok := rawdb.IsAccountTrieNode(key)
|
|
||||||
if ok {
|
|
||||||
return true, crypto.Keccak256Hash(val)
|
|
||||||
}
|
|
||||||
ok = rawdb.IsStorageTrieNode(key)
|
|
||||||
if ok {
|
|
||||||
return true, crypto.Keccak256Hash(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, common.Hash{}
|
|
||||||
}
|
|
||||||
|
|
@ -1,301 +0,0 @@
|
||||||
// Copyright 2016 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// journalEntry is a modification entry in the state change journal that can be
|
|
||||||
// reverted on demand.
|
|
||||||
type journalEntry interface {
|
|
||||||
// revert undoes the changes introduced by this journal entry.
|
|
||||||
revert(*StateDB)
|
|
||||||
|
|
||||||
// dirtied returns the Ethereum address modified by this journal entry.
|
|
||||||
dirtied() *common.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
// journal contains the list of state modifications applied since the last state
|
|
||||||
// commit. These are tracked to be able to be reverted in the case of an execution
|
|
||||||
// exception or request for reversal.
|
|
||||||
type journal struct {
|
|
||||||
entries []journalEntry // Current changes tracked by the journal
|
|
||||||
dirties map[common.Address]int // Dirty accounts and the number of changes
|
|
||||||
}
|
|
||||||
|
|
||||||
// newJournal creates a new initialized journal.
|
|
||||||
func newJournal() *journal {
|
|
||||||
return &journal{
|
|
||||||
dirties: make(map[common.Address]int),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// append inserts a new modification entry to the end of the change journal.
|
|
||||||
func (j *journal) append(entry journalEntry) {
|
|
||||||
j.entries = append(j.entries, entry)
|
|
||||||
if addr := entry.dirtied(); addr != nil {
|
|
||||||
j.dirties[*addr]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// revert undoes a batch of journalled modifications along with any reverted
|
|
||||||
// dirty handling too.
|
|
||||||
func (j *journal) revert(statedb *StateDB, snapshot int) {
|
|
||||||
for i := len(j.entries) - 1; i >= snapshot; i-- {
|
|
||||||
// Undo the changes made by the operation
|
|
||||||
j.entries[i].revert(statedb)
|
|
||||||
|
|
||||||
// Drop any dirty tracking induced by the change
|
|
||||||
if addr := j.entries[i].dirtied(); addr != nil {
|
|
||||||
if j.dirties[*addr]--; j.dirties[*addr] == 0 {
|
|
||||||
delete(j.dirties, *addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
j.entries = j.entries[:snapshot]
|
|
||||||
}
|
|
||||||
|
|
||||||
// dirty explicitly sets an address to dirty, even if the change entries would
|
|
||||||
// otherwise suggest it as clean. This method is an ugly hack to handle the RIPEMD
|
|
||||||
// precompile consensus exception.
|
|
||||||
func (j *journal) dirty(addr common.Address) {
|
|
||||||
j.dirties[addr]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// length returns the current number of entries in the journal.
|
|
||||||
func (j *journal) length() int {
|
|
||||||
return len(j.entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
// Changes to the account trie.
|
|
||||||
createObjectChange struct {
|
|
||||||
account *common.Address
|
|
||||||
}
|
|
||||||
resetObjectChange struct {
|
|
||||||
account *common.Address
|
|
||||||
prev *stateObject
|
|
||||||
prevdestruct bool
|
|
||||||
prevAccount []byte
|
|
||||||
prevStorage map[common.Hash][]byte
|
|
||||||
|
|
||||||
prevAccountOriginExist bool
|
|
||||||
prevAccountOrigin []byte
|
|
||||||
prevStorageOrigin map[common.Hash][]byte
|
|
||||||
}
|
|
||||||
selfDestructChange struct {
|
|
||||||
account *common.Address
|
|
||||||
prev bool // whether account had already self-destructed
|
|
||||||
prevbalance *big.Int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changes to individual accounts.
|
|
||||||
balanceChange struct {
|
|
||||||
account *common.Address
|
|
||||||
prev *big.Int
|
|
||||||
}
|
|
||||||
nonceChange struct {
|
|
||||||
account *common.Address
|
|
||||||
prev uint64
|
|
||||||
}
|
|
||||||
storageChange struct {
|
|
||||||
account *common.Address
|
|
||||||
key, prevalue common.Hash
|
|
||||||
}
|
|
||||||
codeChange struct {
|
|
||||||
account *common.Address
|
|
||||||
prevcode, prevhash []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changes to other state values.
|
|
||||||
refundChange struct {
|
|
||||||
prev uint64
|
|
||||||
}
|
|
||||||
addLogChange struct {
|
|
||||||
txhash common.Hash
|
|
||||||
}
|
|
||||||
addPreimageChange struct {
|
|
||||||
hash common.Hash
|
|
||||||
}
|
|
||||||
touchChange struct {
|
|
||||||
account *common.Address
|
|
||||||
}
|
|
||||||
// Changes to the access list
|
|
||||||
accessListAddAccountChange struct {
|
|
||||||
address *common.Address
|
|
||||||
}
|
|
||||||
accessListAddSlotChange struct {
|
|
||||||
address *common.Address
|
|
||||||
slot *common.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
transientStorageChange struct {
|
|
||||||
account *common.Address
|
|
||||||
key, prevalue common.Hash
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ch createObjectChange) revert(s *StateDB) {
|
|
||||||
delete(s.stateObjects, *ch.account)
|
|
||||||
delete(s.stateObjectsDirty, *ch.account)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch createObjectChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch resetObjectChange) revert(s *StateDB) {
|
|
||||||
s.setStateObject(ch.prev)
|
|
||||||
if !ch.prevdestruct {
|
|
||||||
delete(s.stateObjectsDestruct, ch.prev.address)
|
|
||||||
}
|
|
||||||
if ch.prevAccount != nil {
|
|
||||||
s.accounts[ch.prev.addrHash] = ch.prevAccount
|
|
||||||
}
|
|
||||||
if ch.prevStorage != nil {
|
|
||||||
s.storages[ch.prev.addrHash] = ch.prevStorage
|
|
||||||
}
|
|
||||||
if ch.prevAccountOriginExist {
|
|
||||||
s.accountsOrigin[ch.prev.address] = ch.prevAccountOrigin
|
|
||||||
}
|
|
||||||
if ch.prevStorageOrigin != nil {
|
|
||||||
s.storagesOrigin[ch.prev.address] = ch.prevStorageOrigin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch resetObjectChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch selfDestructChange) revert(s *StateDB) {
|
|
||||||
obj := s.getStateObject(*ch.account)
|
|
||||||
if obj != nil {
|
|
||||||
obj.selfDestructed = ch.prev
|
|
||||||
obj.setBalance(ch.prevbalance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch selfDestructChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
var ripemd = common.HexToAddress("0000000000000000000000000000000000000003")
|
|
||||||
|
|
||||||
func (ch touchChange) revert(s *StateDB) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch touchChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch balanceChange) revert(s *StateDB) {
|
|
||||||
s.getStateObject(*ch.account).setBalance(ch.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch balanceChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch nonceChange) revert(s *StateDB) {
|
|
||||||
s.getStateObject(*ch.account).setNonce(ch.prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch nonceChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch codeChange) revert(s *StateDB) {
|
|
||||||
s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch codeChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch storageChange) revert(s *StateDB) {
|
|
||||||
s.getStateObject(*ch.account).setState(ch.key, ch.prevalue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch storageChange) dirtied() *common.Address {
|
|
||||||
return ch.account
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch transientStorageChange) revert(s *StateDB) {
|
|
||||||
s.setTransientState(*ch.account, ch.key, ch.prevalue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch transientStorageChange) dirtied() *common.Address {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch refundChange) revert(s *StateDB) {
|
|
||||||
s.refund = ch.prev
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch refundChange) dirtied() *common.Address {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch addLogChange) revert(s *StateDB) {
|
|
||||||
logs := s.logs[ch.txhash]
|
|
||||||
if len(logs) == 1 {
|
|
||||||
delete(s.logs, ch.txhash)
|
|
||||||
} else {
|
|
||||||
s.logs[ch.txhash] = logs[:len(logs)-1]
|
|
||||||
}
|
|
||||||
s.logSize--
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch addLogChange) dirtied() *common.Address {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch addPreimageChange) revert(s *StateDB) {
|
|
||||||
delete(s.preimages, ch.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch addPreimageChange) dirtied() *common.Address {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch accessListAddAccountChange) revert(s *StateDB) {
|
|
||||||
/*
|
|
||||||
One important invariant here, is that whenever a (addr, slot) is added, if the
|
|
||||||
addr is not already present, the add causes two journal entries:
|
|
||||||
- one for the address,
|
|
||||||
- one for the (address,slot)
|
|
||||||
Therefore, when unrolling the change, we can always blindly delete the
|
|
||||||
(addr) at this point, since no storage adds can remain when come upon
|
|
||||||
a single (addr) change.
|
|
||||||
*/
|
|
||||||
s.accessList.DeleteAddress(*ch.address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch accessListAddAccountChange) dirtied() *common.Address {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch accessListAddSlotChange) revert(s *StateDB) {
|
|
||||||
s.accessList.DeleteSlot(*ch.address, *ch.slot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch accessListAddSlotChange) dirtied() *common.Address {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package state
|
|
||||||
|
|
||||||
import "github.com/ethereum/go-ethereum/metrics"
|
|
||||||
|
|
||||||
var (
|
|
||||||
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
|
|
||||||
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
|
|
||||||
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
|
|
||||||
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
|
|
||||||
accountTrieUpdatedMeter = metrics.NewRegisteredMeter("state/update/accountnodes", nil)
|
|
||||||
storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil)
|
|
||||||
accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil)
|
|
||||||
storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil)
|
|
||||||
|
|
||||||
slotDeletionMaxCount = metrics.NewRegisteredGauge("state/delete/storage/max/slot", nil)
|
|
||||||
slotDeletionMaxSize = metrics.NewRegisteredGauge("state/delete/storage/max/size", nil)
|
|
||||||
slotDeletionTimer = metrics.NewRegisteredResettingTimer("state/delete/storage/timer", nil)
|
|
||||||
slotDeletionCount = metrics.NewRegisteredMeter("state/delete/storage/slot", nil)
|
|
||||||
slotDeletionSize = metrics.NewRegisteredMeter("state/delete/storage/size", nil)
|
|
||||||
slotDeletionSkip = metrics.NewRegisteredGauge("state/delete/storage/skip", nil)
|
|
||||||
)
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package pruner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
bloomfilter "github.com/holiman/bloomfilter/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// stateBloomHasher 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 or
|
|
||||||
// contract code hash into a 64 bit mini hash.
|
|
||||||
type stateBloomHasher []byte
|
|
||||||
|
|
||||||
func (f stateBloomHasher) Write(p []byte) (n int, err error) { panic("not implemented") }
|
|
||||||
func (f stateBloomHasher) Sum(b []byte) []byte { panic("not implemented") }
|
|
||||||
func (f stateBloomHasher) Reset() { panic("not implemented") }
|
|
||||||
func (f stateBloomHasher) BlockSize() int { panic("not implemented") }
|
|
||||||
func (f stateBloomHasher) Size() int { return 8 }
|
|
||||||
func (f stateBloomHasher) Sum64() uint64 { return binary.BigEndian.Uint64(f) }
|
|
||||||
|
|
||||||
// stateBloom is a bloom filter used during the state conversion(snapshot->state).
|
|
||||||
// The keys of all generated entries will be recorded here so that in the pruning
|
|
||||||
// stage the entries belong to the specific version can be avoided for deletion.
|
|
||||||
//
|
|
||||||
// The false-positive is allowed here. The "false-positive" entries means they
|
|
||||||
// actually don't belong to the specific version but they are not deleted in the
|
|
||||||
// pruning. The downside of the false-positive allowance is we may leave some "dangling"
|
|
||||||
// nodes in the disk. But in practice the it's very unlike the dangling node is
|
|
||||||
// state root. So in theory this pruned state shouldn't be visited anymore. Another
|
|
||||||
// potential issue is for fast sync. If we do another fast sync upon the pruned
|
|
||||||
// database, it's problematic which will stop the expansion during the syncing.
|
|
||||||
// TODO address it @rjl493456442 @holiman @karalabe.
|
|
||||||
//
|
|
||||||
// After the entire state is generated, the bloom filter should be persisted into
|
|
||||||
// the disk. It indicates the whole generation procedure is finished.
|
|
||||||
type stateBloom struct {
|
|
||||||
bloom *bloomfilter.Filter
|
|
||||||
}
|
|
||||||
|
|
||||||
// newStateBloomWithSize creates a brand new state bloom for state generation.
|
|
||||||
// The bloom filter will be created by the passing bloom filter size. According
|
|
||||||
// to the https://hur.st/bloomfilter/?n=600000000&p=&m=2048MB&k=4, the parameters
|
|
||||||
// are picked so that the false-positive rate for mainnet is low enough.
|
|
||||||
func newStateBloomWithSize(size uint64) (*stateBloom, error) {
|
|
||||||
bloom, err := bloomfilter.New(size*1024*1024*8, 4)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Info("Initialized state bloom", "size", common.StorageSize(float64(bloom.M()/8)))
|
|
||||||
return &stateBloom{bloom: bloom}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStateBloomFromDisk loads the state bloom from the given file.
|
|
||||||
// In this case the assumption is held the bloom filter is complete.
|
|
||||||
func NewStateBloomFromDisk(filename string) (*stateBloom, error) {
|
|
||||||
bloom, _, err := bloomfilter.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &stateBloom{bloom: bloom}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit flushes the bloom filter content into the disk and marks the bloom
|
|
||||||
// as complete.
|
|
||||||
func (bloom *stateBloom) Commit(filename, tempname string) error {
|
|
||||||
// Write the bloom out into a temporary file
|
|
||||||
_, err := bloom.bloom.WriteFile(tempname)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Ensure the file is synced to disk
|
|
||||||
f, err := os.OpenFile(tempname, os.O_RDWR, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := f.Sync(); err != nil {
|
|
||||||
f.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
// Move the temporary file into it's final location
|
|
||||||
return os.Rename(tempname, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put implements the KeyValueWriter interface. But here only the key is needed.
|
|
||||||
func (bloom *stateBloom) Put(key []byte, value []byte) error {
|
|
||||||
// If the key length is not 32bytes, ensure it's contract code
|
|
||||||
// entry with new scheme.
|
|
||||||
if len(key) != common.HashLength {
|
|
||||||
isCode, codeKey := rawdb.IsCodeKey(key)
|
|
||||||
if !isCode {
|
|
||||||
return errors.New("invalid entry")
|
|
||||||
}
|
|
||||||
bloom.bloom.Add(stateBloomHasher(codeKey))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
bloom.bloom.Add(stateBloomHasher(key))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the key from the key-value data store.
|
|
||||||
func (bloom *stateBloom) Delete(key []byte) error { panic("not supported") }
|
|
||||||
|
|
||||||
// Contain is the wrapper of the underlying contains function which
|
|
||||||
// reports whether the key is contained.
|
|
||||||
// - If it says yes, the key may be contained
|
|
||||||
// - If it says no, the key is definitely not contained.
|
|
||||||
func (bloom *stateBloom) Contain(key []byte) bool {
|
|
||||||
return bloom.bloom.Contains(stateBloomHasher(key))
|
|
||||||
}
|
|
||||||
|
|
@ -1,492 +0,0 @@
|
||||||
// Copyright 2021 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package pruner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/state/snapshot"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// stateBloomFilePrefix is the filename prefix of state bloom filter.
|
|
||||||
stateBloomFilePrefix = "statebloom"
|
|
||||||
|
|
||||||
// stateBloomFilePrefix is the filename suffix of state bloom filter.
|
|
||||||
stateBloomFileSuffix = "bf.gz"
|
|
||||||
|
|
||||||
// stateBloomFileTempSuffix is the filename suffix of state bloom filter
|
|
||||||
// while it is being written out to detect write aborts.
|
|
||||||
stateBloomFileTempSuffix = ".tmp"
|
|
||||||
|
|
||||||
// rangeCompactionThreshold is the minimal deleted entry number for
|
|
||||||
// triggering range compaction. It's a quite arbitrary number but just
|
|
||||||
// to avoid triggering range compaction because of small deletion.
|
|
||||||
rangeCompactionThreshold = 100000
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config includes all the configurations for pruning.
|
|
||||||
type Config struct {
|
|
||||||
Datadir string // The directory of the state database
|
|
||||||
BloomSize uint64 // The Megabytes of memory allocated to bloom-filter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pruner is an offline tool to prune the stale state with the
|
|
||||||
// help of the snapshot. The workflow of pruner is very simple:
|
|
||||||
//
|
|
||||||
// - iterate the snapshot, reconstruct the relevant state
|
|
||||||
// - iterate the database, delete all other state entries which
|
|
||||||
// don't belong to the target state and the genesis state
|
|
||||||
//
|
|
||||||
// It can take several hours(around 2 hours for mainnet) to finish
|
|
||||||
// the whole pruning work. It's recommended to run this offline tool
|
|
||||||
// periodically in order to release the disk usage and improve the
|
|
||||||
// disk read performance to some extent.
|
|
||||||
type Pruner struct {
|
|
||||||
config Config
|
|
||||||
chainHeader *types.Header
|
|
||||||
db ethdb.Database
|
|
||||||
stateBloom *stateBloom
|
|
||||||
snaptree *snapshot.Tree
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPruner creates the pruner instance.
|
|
||||||
func NewPruner(db ethdb.Database, config Config) (*Pruner, error) {
|
|
||||||
headBlock := rawdb.ReadHeadBlock(db)
|
|
||||||
if headBlock == nil {
|
|
||||||
return nil, errors.New("failed to load head block")
|
|
||||||
}
|
|
||||||
// Offline pruning is only supported in legacy hash based scheme.
|
|
||||||
triedb := trie.NewDatabase(db, trie.HashDefaults)
|
|
||||||
|
|
||||||
snapconfig := snapshot.Config{
|
|
||||||
CacheSize: 256,
|
|
||||||
Recovery: false,
|
|
||||||
NoBuild: true,
|
|
||||||
AsyncBuild: false,
|
|
||||||
}
|
|
||||||
snaptree, err := snapshot.New(snapconfig, db, triedb, headBlock.Root())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err // The relevant snapshot(s) might not exist
|
|
||||||
}
|
|
||||||
// Sanitize the bloom filter size if it's too small.
|
|
||||||
if config.BloomSize < 256 {
|
|
||||||
log.Warn("Sanitizing bloomfilter size", "provided(MB)", config.BloomSize, "updated(MB)", 256)
|
|
||||||
config.BloomSize = 256
|
|
||||||
}
|
|
||||||
stateBloom, err := newStateBloomWithSize(config.BloomSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Pruner{
|
|
||||||
config: config,
|
|
||||||
chainHeader: headBlock.Header(),
|
|
||||||
db: db,
|
|
||||||
stateBloom: stateBloom,
|
|
||||||
snaptree: snaptree,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, stateBloom *stateBloom, bloomPath string, middleStateRoots map[common.Hash]struct{}, start time.Time) error {
|
|
||||||
// Delete all stale trie nodes in the disk. With the help of state bloom
|
|
||||||
// the trie nodes(and codes) belong to the active state will be filtered
|
|
||||||
// out. A very small part of stale tries will also be filtered because of
|
|
||||||
// the false-positive rate of bloom filter. But the assumption is held here
|
|
||||||
// that the false-positive is low enough(~0.05%). The probablity of the
|
|
||||||
// dangling node is the state root is super low. So the dangling nodes in
|
|
||||||
// theory will never ever be visited again.
|
|
||||||
var (
|
|
||||||
skipped, count int
|
|
||||||
size common.StorageSize
|
|
||||||
pstart = time.Now()
|
|
||||||
logged = time.Now()
|
|
||||||
batch = maindb.NewBatch()
|
|
||||||
iter = maindb.NewIterator(nil, nil)
|
|
||||||
)
|
|
||||||
for iter.Next() {
|
|
||||||
key := iter.Key()
|
|
||||||
|
|
||||||
// All state entries don't belong to specific state and genesis are deleted here
|
|
||||||
// - trie node
|
|
||||||
// - legacy contract code
|
|
||||||
// - new-scheme contract code
|
|
||||||
isCode, codeKey := rawdb.IsCodeKey(key)
|
|
||||||
if len(key) == common.HashLength || isCode {
|
|
||||||
checkKey := key
|
|
||||||
if isCode {
|
|
||||||
checkKey = codeKey
|
|
||||||
}
|
|
||||||
if _, exist := middleStateRoots[common.BytesToHash(checkKey)]; exist {
|
|
||||||
log.Debug("Forcibly delete the middle state roots", "hash", common.BytesToHash(checkKey))
|
|
||||||
} else {
|
|
||||||
if stateBloom.Contain(checkKey) {
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count += 1
|
|
||||||
size += common.StorageSize(len(key) + len(iter.Value()))
|
|
||||||
batch.Delete(key)
|
|
||||||
|
|
||||||
var eta time.Duration // Realistically will never remain uninited
|
|
||||||
if done := binary.BigEndian.Uint64(key[:8]); done > 0 {
|
|
||||||
var (
|
|
||||||
left = math.MaxUint64 - binary.BigEndian.Uint64(key[:8])
|
|
||||||
speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
|
||||||
)
|
|
||||||
eta = time.Duration(left/speed) * time.Millisecond
|
|
||||||
}
|
|
||||||
if time.Since(logged) > 8*time.Second {
|
|
||||||
log.Info("Pruning state data", "nodes", count, "skipped", skipped, "size", size,
|
|
||||||
"elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
// Recreate the iterator after every batch commit in order
|
|
||||||
// to allow the underlying compactor to delete the entries.
|
|
||||||
if batch.ValueSize() >= ethdb.IdealBatchSize {
|
|
||||||
batch.Write()
|
|
||||||
batch.Reset()
|
|
||||||
|
|
||||||
iter.Release()
|
|
||||||
iter = maindb.NewIterator(nil, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if batch.ValueSize() > 0 {
|
|
||||||
batch.Write()
|
|
||||||
batch.Reset()
|
|
||||||
}
|
|
||||||
iter.Release()
|
|
||||||
log.Info("Pruned state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart)))
|
|
||||||
|
|
||||||
// Pruning is done, now drop the "useless" layers from the snapshot.
|
|
||||||
// Firstly, flushing the target layer into the disk. After that all
|
|
||||||
// diff layers below the target will all be merged into the disk.
|
|
||||||
if err := snaptree.Cap(root, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Secondly, flushing the snapshot journal into the disk. All diff
|
|
||||||
// layers upon are dropped silently. Eventually the entire snapshot
|
|
||||||
// tree is converted into a single disk layer with the pruning target
|
|
||||||
// as the root.
|
|
||||||
if _, err := snaptree.Journal(root); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Delete the state bloom, it marks the entire pruning procedure is
|
|
||||||
// finished. If any crashes or manual exit happens before this,
|
|
||||||
// `RecoverPruning` will pick it up in the next restarts to redo all
|
|
||||||
// the things.
|
|
||||||
os.RemoveAll(bloomPath)
|
|
||||||
|
|
||||||
// Start compactions, will remove the deleted data from the disk immediately.
|
|
||||||
// Note for small pruning, the compaction is skipped.
|
|
||||||
if count >= rangeCompactionThreshold {
|
|
||||||
cstart := time.Now()
|
|
||||||
for b := 0x00; b <= 0xf0; b += 0x10 {
|
|
||||||
var (
|
|
||||||
start = []byte{byte(b)}
|
|
||||||
end = []byte{byte(b + 0x10)}
|
|
||||||
)
|
|
||||||
if b == 0xf0 {
|
|
||||||
end = nil
|
|
||||||
}
|
|
||||||
log.Info("Compacting database", "range", fmt.Sprintf("%#x-%#x", start, end), "elapsed", common.PrettyDuration(time.Since(cstart)))
|
|
||||||
if err := maindb.Compact(start, end); err != nil {
|
|
||||||
log.Error("Database compaction failed", "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info("Database compaction finished", "elapsed", common.PrettyDuration(time.Since(cstart)))
|
|
||||||
}
|
|
||||||
log.Info("State pruning successful", "pruned", size, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune deletes all historical state nodes except the nodes belong to the
|
|
||||||
// specified state version. If user doesn't specify the state version, use
|
|
||||||
// the bottom-most snapshot diff layer as the target.
|
|
||||||
func (p *Pruner) Prune(root common.Hash) error {
|
|
||||||
// If the state bloom filter is already committed previously,
|
|
||||||
// reuse it for pruning instead of generating a new one. It's
|
|
||||||
// mandatory because a part of state may already be deleted,
|
|
||||||
// the recovery procedure is necessary.
|
|
||||||
_, stateBloomRoot, err := findBloomFilter(p.config.Datadir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if stateBloomRoot != (common.Hash{}) {
|
|
||||||
return RecoverPruning(p.config.Datadir, p.db)
|
|
||||||
}
|
|
||||||
// If the target state root is not specified, use the HEAD-127 as the
|
|
||||||
// target. The reason for picking it is:
|
|
||||||
// - in most of the normal cases, the related state is available
|
|
||||||
// - the probability of this layer being reorg is very low
|
|
||||||
var layers []snapshot.Snapshot
|
|
||||||
if root == (common.Hash{}) {
|
|
||||||
// Retrieve all snapshot layers from the current HEAD.
|
|
||||||
// In theory there are 128 difflayers + 1 disk layer present,
|
|
||||||
// so 128 diff layers are expected to be returned.
|
|
||||||
layers = p.snaptree.Snapshots(p.chainHeader.Root, 128, true)
|
|
||||||
if len(layers) != 128 {
|
|
||||||
// Reject if the accumulated diff layers are less than 128. It
|
|
||||||
// means in most of normal cases, there is no associated state
|
|
||||||
// with bottom-most diff layer.
|
|
||||||
return fmt.Errorf("snapshot not old enough yet: need %d more blocks", 128-len(layers))
|
|
||||||
}
|
|
||||||
// Use the bottom-most diff layer as the target
|
|
||||||
root = layers[len(layers)-1].Root()
|
|
||||||
}
|
|
||||||
// Ensure the root is really present. The weak assumption
|
|
||||||
// is the presence of root can indicate the presence of the
|
|
||||||
// entire trie.
|
|
||||||
if !rawdb.HasLegacyTrieNode(p.db, root) {
|
|
||||||
// The special case is for clique based networks(goerli
|
|
||||||
// and some other private networks), it's possible that two
|
|
||||||
// consecutive blocks will have same root. In this case snapshot
|
|
||||||
// difflayer won't be created. So HEAD-127 may not paired with
|
|
||||||
// head-127 layer. Instead the paired layer is higher than the
|
|
||||||
// bottom-most diff layer. Try to find the bottom-most snapshot
|
|
||||||
// layer with state available.
|
|
||||||
//
|
|
||||||
// Note HEAD and HEAD-1 is ignored. Usually there is the associated
|
|
||||||
// state available, but we don't want to use the topmost state
|
|
||||||
// as the pruning target.
|
|
||||||
var found bool
|
|
||||||
for i := len(layers) - 2; i >= 2; i-- {
|
|
||||||
if rawdb.HasLegacyTrieNode(p.db, layers[i].Root()) {
|
|
||||||
root = layers[i].Root()
|
|
||||||
found = true
|
|
||||||
log.Info("Selecting middle-layer as the pruning target", "root", root, "depth", i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
if len(layers) > 0 {
|
|
||||||
return errors.New("no snapshot paired state")
|
|
||||||
}
|
|
||||||
return fmt.Errorf("associated state[%x] is not present", root)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(layers) > 0 {
|
|
||||||
log.Info("Selecting bottom-most difflayer as the pruning target", "root", root, "height", p.chainHeader.Number.Uint64()-127)
|
|
||||||
} else {
|
|
||||||
log.Info("Selecting user-specified state as the pruning target", "root", root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// All the state roots of the middle layer should be forcibly pruned,
|
|
||||||
// otherwise the dangling state will be left.
|
|
||||||
middleRoots := make(map[common.Hash]struct{})
|
|
||||||
for _, layer := range layers {
|
|
||||||
if layer.Root() == root {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
middleRoots[layer.Root()] = struct{}{}
|
|
||||||
}
|
|
||||||
// Traverse the target state, re-construct the whole state trie and
|
|
||||||
// commit to the given bloom filter.
|
|
||||||
start := time.Now()
|
|
||||||
if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Traverse the genesis, put all genesis state entries into the
|
|
||||||
// bloom filter too.
|
|
||||||
if err := extractGenesis(p.db, p.stateBloom); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filterName := bloomFilterName(p.config.Datadir, root)
|
|
||||||
|
|
||||||
log.Info("Writing state bloom to disk", "name", filterName)
|
|
||||||
if err := p.stateBloom.Commit(filterName, filterName+stateBloomFileTempSuffix); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("State bloom filter committed", "name", filterName)
|
|
||||||
return prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecoverPruning will resume the pruning procedure during the system restart.
|
|
||||||
// This function is used in this case: user tries to prune state data, but the
|
|
||||||
// system was interrupted midway because of crash or manual-kill. In this case
|
|
||||||
// if the bloom filter for filtering active state is already constructed, the
|
|
||||||
// pruning can be resumed. What's more if the bloom filter is constructed, the
|
|
||||||
// pruning **has to be resumed**. Otherwise a lot of dangling nodes may be left
|
|
||||||
// in the disk.
|
|
||||||
func RecoverPruning(datadir string, db ethdb.Database) error {
|
|
||||||
stateBloomPath, stateBloomRoot, err := findBloomFilter(datadir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if stateBloomPath == "" {
|
|
||||||
return nil // nothing to recover
|
|
||||||
}
|
|
||||||
headBlock := rawdb.ReadHeadBlock(db)
|
|
||||||
if headBlock == nil {
|
|
||||||
return errors.New("failed to load head block")
|
|
||||||
}
|
|
||||||
// Initialize the snapshot tree in recovery mode to handle this special case:
|
|
||||||
// - Users run the `prune-state` command multiple times
|
|
||||||
// - Neither these `prune-state` running is finished(e.g. interrupted manually)
|
|
||||||
// - The state bloom filter is already generated, a part of state is deleted,
|
|
||||||
// so that resuming the pruning here is mandatory
|
|
||||||
// - The state HEAD is rewound already because of multiple incomplete `prune-state`
|
|
||||||
// In this case, even the state HEAD is not exactly matched with snapshot, it
|
|
||||||
// still feasible to recover the pruning correctly.
|
|
||||||
snapconfig := snapshot.Config{
|
|
||||||
CacheSize: 256,
|
|
||||||
Recovery: true,
|
|
||||||
NoBuild: true,
|
|
||||||
AsyncBuild: false,
|
|
||||||
}
|
|
||||||
// Offline pruning is only supported in legacy hash based scheme.
|
|
||||||
triedb := trie.NewDatabase(db, trie.HashDefaults)
|
|
||||||
snaptree, err := snapshot.New(snapconfig, db, triedb, headBlock.Root())
|
|
||||||
if err != nil {
|
|
||||||
return err // The relevant snapshot(s) might not exist
|
|
||||||
}
|
|
||||||
stateBloom, err := NewStateBloomFromDisk(stateBloomPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("Loaded state bloom filter", "path", stateBloomPath)
|
|
||||||
|
|
||||||
// All the state roots of the middle layers should be forcibly pruned,
|
|
||||||
// otherwise the dangling state will be left.
|
|
||||||
var (
|
|
||||||
found bool
|
|
||||||
layers = snaptree.Snapshots(headBlock.Root(), 128, true)
|
|
||||||
middleRoots = make(map[common.Hash]struct{})
|
|
||||||
)
|
|
||||||
for _, layer := range layers {
|
|
||||||
if layer.Root() == stateBloomRoot {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
middleRoots[layer.Root()] = struct{}{}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
log.Error("Pruning target state is not existent")
|
|
||||||
return errors.New("non-existent target state")
|
|
||||||
}
|
|
||||||
return prune(snaptree, stateBloomRoot, db, stateBloom, stateBloomPath, middleRoots, time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractGenesis loads the genesis state and commits all the state entries
|
|
||||||
// into the given bloomfilter.
|
|
||||||
func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error {
|
|
||||||
genesisHash := rawdb.ReadCanonicalHash(db, 0)
|
|
||||||
if genesisHash == (common.Hash{}) {
|
|
||||||
return errors.New("missing genesis hash")
|
|
||||||
}
|
|
||||||
genesis := rawdb.ReadBlock(db, genesisHash, 0)
|
|
||||||
if genesis == nil {
|
|
||||||
return errors.New("missing genesis block")
|
|
||||||
}
|
|
||||||
t, err := trie.NewStateTrie(trie.StateTrieID(genesis.Root()), trie.NewDatabase(db, trie.HashDefaults))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
accIter, err := t.NodeIterator(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for accIter.Next(true) {
|
|
||||||
hash := accIter.Hash()
|
|
||||||
|
|
||||||
// Embedded nodes don't have hash.
|
|
||||||
if hash != (common.Hash{}) {
|
|
||||||
stateBloom.Put(hash.Bytes(), nil)
|
|
||||||
}
|
|
||||||
// If it's a leaf node, yes we are touching an account,
|
|
||||||
// dig into the storage trie further.
|
|
||||||
if accIter.Leaf() {
|
|
||||||
var acc types.StateAccount
|
|
||||||
if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if acc.Root != types.EmptyRootHash {
|
|
||||||
id := trie.StorageTrieID(genesis.Root(), common.BytesToHash(accIter.LeafKey()), acc.Root)
|
|
||||||
storageTrie, err := trie.NewStateTrie(id, trie.NewDatabase(db, trie.HashDefaults))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
storageIter, err := storageTrie.NodeIterator(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for storageIter.Next(true) {
|
|
||||||
hash := storageIter.Hash()
|
|
||||||
if hash != (common.Hash{}) {
|
|
||||||
stateBloom.Put(hash.Bytes(), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if storageIter.Error() != nil {
|
|
||||||
return storageIter.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) {
|
|
||||||
stateBloom.Put(acc.CodeHash, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return accIter.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func bloomFilterName(datadir string, hash common.Hash) string {
|
|
||||||
return filepath.Join(datadir, fmt.Sprintf("%s.%s.%s", stateBloomFilePrefix, hash.Hex(), stateBloomFileSuffix))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBloomFilter(filename string) (bool, common.Hash) {
|
|
||||||
filename = filepath.Base(filename)
|
|
||||||
if strings.HasPrefix(filename, stateBloomFilePrefix) && strings.HasSuffix(filename, stateBloomFileSuffix) {
|
|
||||||
return true, common.HexToHash(filename[len(stateBloomFilePrefix)+1 : len(filename)-len(stateBloomFileSuffix)-1])
|
|
||||||
}
|
|
||||||
return false, common.Hash{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findBloomFilter(datadir string) (string, common.Hash, error) {
|
|
||||||
var (
|
|
||||||
stateBloomPath string
|
|
||||||
stateBloomRoot common.Hash
|
|
||||||
)
|
|
||||||
if err := filepath.Walk(datadir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if info != nil && !info.IsDir() {
|
|
||||||
ok, root := isBloomFilter(path)
|
|
||||||
if ok {
|
|
||||||
stateBloomPath = path
|
|
||||||
stateBloomRoot = root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return "", common.Hash{}, err
|
|
||||||
}
|
|
||||||
return stateBloomPath, stateBloomRoot, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
snapAccount = "account" // Identifier of account snapshot generation
|
|
||||||
snapStorage = "storage" // Identifier of storage snapshot generation
|
|
||||||
)
|
|
||||||
|
|
||||||
// generatorStats is a collection of statistics gathered by the snapshot generator
|
|
||||||
// for logging purposes.
|
|
||||||
type generatorStats struct {
|
|
||||||
origin uint64 // Origin prefix where generation started
|
|
||||||
start time.Time // Timestamp when generation started
|
|
||||||
accounts uint64 // Number of accounts indexed(generated or recovered)
|
|
||||||
slots uint64 // Number of storage slots indexed(generated or recovered)
|
|
||||||
dangling uint64 // Number of dangling storage slots
|
|
||||||
storage common.StorageSize // Total account and storage slot size(generation or recovery)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log creates an contextual log with the given message and the context pulled
|
|
||||||
// from the internally maintained statistics.
|
|
||||||
func (gs *generatorStats) Log(msg string, root common.Hash, marker []byte) {
|
|
||||||
var ctx []interface{}
|
|
||||||
if root != (common.Hash{}) {
|
|
||||||
ctx = append(ctx, []interface{}{"root", root}...)
|
|
||||||
}
|
|
||||||
// Figure out whether we're after or within an account
|
|
||||||
switch len(marker) {
|
|
||||||
case common.HashLength:
|
|
||||||
ctx = append(ctx, []interface{}{"at", common.BytesToHash(marker)}...)
|
|
||||||
case 2 * common.HashLength:
|
|
||||||
ctx = append(ctx, []interface{}{
|
|
||||||
"in", common.BytesToHash(marker[:common.HashLength]),
|
|
||||||
"at", common.BytesToHash(marker[common.HashLength:]),
|
|
||||||
}...)
|
|
||||||
}
|
|
||||||
// Add the usual measurements
|
|
||||||
ctx = append(ctx, []interface{}{
|
|
||||||
"accounts", gs.accounts,
|
|
||||||
"slots", gs.slots,
|
|
||||||
"storage", gs.storage,
|
|
||||||
"dangling", gs.dangling,
|
|
||||||
"elapsed", common.PrettyDuration(time.Since(gs.start)),
|
|
||||||
}...)
|
|
||||||
// Calculate the estimated indexing time based on current stats
|
|
||||||
if len(marker) > 0 {
|
|
||||||
if done := binary.BigEndian.Uint64(marker[:8]) - gs.origin; done > 0 {
|
|
||||||
left := math.MaxUint64 - binary.BigEndian.Uint64(marker[:8])
|
|
||||||
|
|
||||||
speed := done/uint64(time.Since(gs.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
|
||||||
ctx = append(ctx, []interface{}{
|
|
||||||
"eta", common.PrettyDuration(time.Duration(left/speed) * time.Millisecond),
|
|
||||||
}...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(msg, ctx...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generatorContext carries a few global values to be shared by all generation functions.
|
|
||||||
type generatorContext struct {
|
|
||||||
stats *generatorStats // Generation statistic collection
|
|
||||||
db ethdb.KeyValueStore // Key-value store containing the snapshot data
|
|
||||||
account *holdableIterator // Iterator of account snapshot data
|
|
||||||
storage *holdableIterator // Iterator of storage snapshot data
|
|
||||||
batch ethdb.Batch // Database batch for writing batch data atomically
|
|
||||||
logged time.Time // The timestamp when last generation progress was displayed
|
|
||||||
}
|
|
||||||
|
|
||||||
// newGeneratorContext initializes the context for generation.
|
|
||||||
func newGeneratorContext(stats *generatorStats, db ethdb.KeyValueStore, accMarker []byte, storageMarker []byte) *generatorContext {
|
|
||||||
ctx := &generatorContext{
|
|
||||||
stats: stats,
|
|
||||||
db: db,
|
|
||||||
batch: db.NewBatch(),
|
|
||||||
logged: time.Now(),
|
|
||||||
}
|
|
||||||
ctx.openIterator(snapAccount, accMarker)
|
|
||||||
ctx.openIterator(snapStorage, storageMarker)
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
// openIterator constructs global account and storage snapshot iterators
|
|
||||||
// at the interrupted position. These iterators should be reopened from time
|
|
||||||
// to time to avoid blocking leveldb compaction for a long time.
|
|
||||||
func (ctx *generatorContext) openIterator(kind string, start []byte) {
|
|
||||||
if kind == snapAccount {
|
|
||||||
iter := ctx.db.NewIterator(rawdb.SnapshotAccountPrefix, start)
|
|
||||||
ctx.account = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+common.HashLength))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
iter := ctx.db.NewIterator(rawdb.SnapshotStoragePrefix, start)
|
|
||||||
ctx.storage = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+2*common.HashLength))
|
|
||||||
}
|
|
||||||
|
|
||||||
// reopenIterator releases the specified snapshot iterator and re-open it
|
|
||||||
// in the next position. It's aimed for not blocking leveldb compaction.
|
|
||||||
func (ctx *generatorContext) reopenIterator(kind string) {
|
|
||||||
// Shift iterator one more step, so that we can reopen
|
|
||||||
// the iterator at the right position.
|
|
||||||
var iter = ctx.account
|
|
||||||
if kind == snapStorage {
|
|
||||||
iter = ctx.storage
|
|
||||||
}
|
|
||||||
hasNext := iter.Next()
|
|
||||||
if !hasNext {
|
|
||||||
// Iterator exhausted, release forever and create an already exhausted virtual iterator
|
|
||||||
iter.Release()
|
|
||||||
if kind == snapAccount {
|
|
||||||
ctx.account = newHoldableIterator(memorydb.New().NewIterator(nil, nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.storage = newHoldableIterator(memorydb.New().NewIterator(nil, nil))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next := iter.Key()
|
|
||||||
iter.Release()
|
|
||||||
ctx.openIterator(kind, next[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// close releases all the held resources.
|
|
||||||
func (ctx *generatorContext) close() {
|
|
||||||
ctx.account.Release()
|
|
||||||
ctx.storage.Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterator returns the corresponding iterator specified by the kind.
|
|
||||||
func (ctx *generatorContext) iterator(kind string) *holdableIterator {
|
|
||||||
if kind == snapAccount {
|
|
||||||
return ctx.account
|
|
||||||
}
|
|
||||||
return ctx.storage
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeStorageBefore deletes all storage entries which are located before
|
|
||||||
// the specified account. When the iterator touches the storage entry which
|
|
||||||
// is located in or outside the given account, it stops and holds the current
|
|
||||||
// iterated element locally.
|
|
||||||
func (ctx *generatorContext) removeStorageBefore(account common.Hash) {
|
|
||||||
var (
|
|
||||||
count uint64
|
|
||||||
start = time.Now()
|
|
||||||
iter = ctx.storage
|
|
||||||
)
|
|
||||||
for iter.Next() {
|
|
||||||
key := iter.Key()
|
|
||||||
if bytes.Compare(key[1:1+common.HashLength], account.Bytes()) >= 0 {
|
|
||||||
iter.Hold()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
ctx.batch.Delete(key)
|
|
||||||
if ctx.batch.ValueSize() > ethdb.IdealBatchSize {
|
|
||||||
ctx.batch.Write()
|
|
||||||
ctx.batch.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.stats.dangling += count
|
|
||||||
snapStorageCleanCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeStorageAt deletes all storage entries which are located in the specified
|
|
||||||
// account. When the iterator touches the storage entry which is outside the given
|
|
||||||
// account, it stops and holds the current iterated element locally. An error will
|
|
||||||
// be returned if the initial position of iterator is not in the given account.
|
|
||||||
func (ctx *generatorContext) removeStorageAt(account common.Hash) error {
|
|
||||||
var (
|
|
||||||
count int64
|
|
||||||
start = time.Now()
|
|
||||||
iter = ctx.storage
|
|
||||||
)
|
|
||||||
for iter.Next() {
|
|
||||||
key := iter.Key()
|
|
||||||
cmp := bytes.Compare(key[1:1+common.HashLength], account.Bytes())
|
|
||||||
if cmp < 0 {
|
|
||||||
return errors.New("invalid iterator position")
|
|
||||||
}
|
|
||||||
if cmp > 0 {
|
|
||||||
iter.Hold()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
ctx.batch.Delete(key)
|
|
||||||
if ctx.batch.ValueSize() > ethdb.IdealBatchSize {
|
|
||||||
ctx.batch.Write()
|
|
||||||
ctx.batch.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snapWipedStorageMeter.Mark(count)
|
|
||||||
snapStorageCleanCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeStorageLeft deletes all storage entries which are located after
|
|
||||||
// the current iterator position.
|
|
||||||
func (ctx *generatorContext) removeStorageLeft() {
|
|
||||||
var (
|
|
||||||
count uint64
|
|
||||||
start = time.Now()
|
|
||||||
iter = ctx.storage
|
|
||||||
)
|
|
||||||
for iter.Next() {
|
|
||||||
count++
|
|
||||||
ctx.batch.Delete(iter.Key())
|
|
||||||
if ctx.batch.ValueSize() > ethdb.IdealBatchSize {
|
|
||||||
ctx.batch.Write()
|
|
||||||
ctx.batch.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.stats.dangling += count
|
|
||||||
snapDanglingStorageMeter.Mark(int64(count))
|
|
||||||
snapStorageCleanCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
}
|
|
||||||
|
|
@ -1,376 +0,0 @@
|
||||||
// Copyright 2020 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// trieKV represents a trie key-value pair
|
|
||||||
type trieKV struct {
|
|
||||||
key common.Hash
|
|
||||||
value []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
// trieGeneratorFn is the interface of trie generation which can
|
|
||||||
// be implemented by different trie algorithm.
|
|
||||||
trieGeneratorFn func(db ethdb.KeyValueWriter, scheme string, owner common.Hash, in chan (trieKV), out chan (common.Hash))
|
|
||||||
|
|
||||||
// leafCallbackFn is the callback invoked at the leaves of the trie,
|
|
||||||
// returns the subtrie root with the specified subtrie identifier.
|
|
||||||
leafCallbackFn func(db ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error)
|
|
||||||
)
|
|
||||||
|
|
||||||
// GenerateAccountTrieRoot takes an account iterator and reproduces the root hash.
|
|
||||||
func GenerateAccountTrieRoot(it AccountIterator) (common.Hash, error) {
|
|
||||||
return generateTrieRoot(nil, "", it, common.Hash{}, stackTrieGenerate, nil, newGenerateStats(), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateStorageTrieRoot takes a storage iterator and reproduces the root hash.
|
|
||||||
func GenerateStorageTrieRoot(account common.Hash, it StorageIterator) (common.Hash, error) {
|
|
||||||
return generateTrieRoot(nil, "", it, account, stackTrieGenerate, nil, newGenerateStats(), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateTrie takes the whole snapshot tree as the input, traverses all the
|
|
||||||
// accounts as well as the corresponding storages and regenerate the whole state
|
|
||||||
// (account trie + all storage tries).
|
|
||||||
func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethdb.KeyValueWriter) error {
|
|
||||||
// Traverse all state by snapshot, re-generate the whole state trie
|
|
||||||
acctIt, err := snaptree.AccountIterator(root, common.Hash{})
|
|
||||||
if err != nil {
|
|
||||||
return err // The required snapshot might not exist.
|
|
||||||
}
|
|
||||||
defer acctIt.Release()
|
|
||||||
|
|
||||||
scheme := snaptree.triedb.Scheme()
|
|
||||||
got, err := generateTrieRoot(dst, scheme, acctIt, common.Hash{}, stackTrieGenerate, func(dst ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error) {
|
|
||||||
// Migrate the code first, commit the contract code into the tmp db.
|
|
||||||
if codeHash != types.EmptyCodeHash {
|
|
||||||
code := rawdb.ReadCode(src, codeHash)
|
|
||||||
if len(code) == 0 {
|
|
||||||
return common.Hash{}, errors.New("failed to read contract code")
|
|
||||||
}
|
|
||||||
rawdb.WriteCode(dst, codeHash, code)
|
|
||||||
}
|
|
||||||
// Then migrate all storage trie nodes into the tmp db.
|
|
||||||
storageIt, err := snaptree.StorageIterator(root, accountHash, common.Hash{})
|
|
||||||
if err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
defer storageIt.Release()
|
|
||||||
|
|
||||||
hash, err := generateTrieRoot(dst, scheme, storageIt, accountHash, stackTrieGenerate, nil, stat, false)
|
|
||||||
if err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
return hash, nil
|
|
||||||
}, newGenerateStats(), true)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if got != root {
|
|
||||||
return fmt.Errorf("state root hash mismatch: got %x, want %x", got, root)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateStats is a collection of statistics gathered by the trie generator
|
|
||||||
// for logging purposes.
|
|
||||||
type generateStats struct {
|
|
||||||
head common.Hash
|
|
||||||
start time.Time
|
|
||||||
|
|
||||||
accounts uint64 // Number of accounts done (including those being crawled)
|
|
||||||
slots uint64 // Number of storage slots done (including those being crawled)
|
|
||||||
|
|
||||||
slotsStart map[common.Hash]time.Time // Start time for account slot crawling
|
|
||||||
slotsHead map[common.Hash]common.Hash // Slot head for accounts being crawled
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// newGenerateStats creates a new generator stats.
|
|
||||||
func newGenerateStats() *generateStats {
|
|
||||||
return &generateStats{
|
|
||||||
slotsStart: make(map[common.Hash]time.Time),
|
|
||||||
slotsHead: make(map[common.Hash]common.Hash),
|
|
||||||
start: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// progressAccounts updates the generator stats for the account range.
|
|
||||||
func (stat *generateStats) progressAccounts(account common.Hash, done uint64) {
|
|
||||||
stat.lock.Lock()
|
|
||||||
defer stat.lock.Unlock()
|
|
||||||
|
|
||||||
stat.accounts += done
|
|
||||||
stat.head = account
|
|
||||||
}
|
|
||||||
|
|
||||||
// finishAccounts updates the generator stats for the finished account range.
|
|
||||||
func (stat *generateStats) finishAccounts(done uint64) {
|
|
||||||
stat.lock.Lock()
|
|
||||||
defer stat.lock.Unlock()
|
|
||||||
|
|
||||||
stat.accounts += done
|
|
||||||
}
|
|
||||||
|
|
||||||
// progressContract updates the generator stats for a specific in-progress contract.
|
|
||||||
func (stat *generateStats) progressContract(account common.Hash, slot common.Hash, done uint64) {
|
|
||||||
stat.lock.Lock()
|
|
||||||
defer stat.lock.Unlock()
|
|
||||||
|
|
||||||
stat.slots += done
|
|
||||||
stat.slotsHead[account] = slot
|
|
||||||
if _, ok := stat.slotsStart[account]; !ok {
|
|
||||||
stat.slotsStart[account] = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finishContract updates the generator stats for a specific just-finished contract.
|
|
||||||
func (stat *generateStats) finishContract(account common.Hash, done uint64) {
|
|
||||||
stat.lock.Lock()
|
|
||||||
defer stat.lock.Unlock()
|
|
||||||
|
|
||||||
stat.slots += done
|
|
||||||
delete(stat.slotsHead, account)
|
|
||||||
delete(stat.slotsStart, account)
|
|
||||||
}
|
|
||||||
|
|
||||||
// report prints the cumulative progress statistic smartly.
|
|
||||||
func (stat *generateStats) report() {
|
|
||||||
stat.lock.RLock()
|
|
||||||
defer stat.lock.RUnlock()
|
|
||||||
|
|
||||||
ctx := []interface{}{
|
|
||||||
"accounts", stat.accounts,
|
|
||||||
"slots", stat.slots,
|
|
||||||
"elapsed", common.PrettyDuration(time.Since(stat.start)),
|
|
||||||
}
|
|
||||||
if stat.accounts > 0 {
|
|
||||||
// If there's progress on the account trie, estimate the time to finish crawling it
|
|
||||||
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
|
|
||||||
var (
|
|
||||||
left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
|
|
||||||
speed = done/uint64(time.Since(stat.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
|
||||||
eta = time.Duration(left/speed) * time.Millisecond
|
|
||||||
)
|
|
||||||
// If there are large contract crawls in progress, estimate their finish time
|
|
||||||
for acc, head := range stat.slotsHead {
|
|
||||||
start := stat.slotsStart[acc]
|
|
||||||
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
|
|
||||||
var (
|
|
||||||
left = math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
|
|
||||||
speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
|
||||||
)
|
|
||||||
// Override the ETA if larger than the largest until now
|
|
||||||
if slotETA := time.Duration(left/speed) * time.Millisecond; eta < slotETA {
|
|
||||||
eta = slotETA
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx = append(ctx, []interface{}{
|
|
||||||
"eta", common.PrettyDuration(eta),
|
|
||||||
}...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info("Iterating state snapshot", ctx...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reportDone prints the last log when the whole generation is finished.
|
|
||||||
func (stat *generateStats) reportDone() {
|
|
||||||
stat.lock.RLock()
|
|
||||||
defer stat.lock.RUnlock()
|
|
||||||
|
|
||||||
var ctx []interface{}
|
|
||||||
ctx = append(ctx, []interface{}{"accounts", stat.accounts}...)
|
|
||||||
if stat.slots != 0 {
|
|
||||||
ctx = append(ctx, []interface{}{"slots", stat.slots}...)
|
|
||||||
}
|
|
||||||
ctx = append(ctx, []interface{}{"elapsed", common.PrettyDuration(time.Since(stat.start))}...)
|
|
||||||
log.Info("Iterated snapshot", ctx...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runReport periodically prints the progress information.
|
|
||||||
func runReport(stats *generateStats, stop chan bool) {
|
|
||||||
timer := time.NewTimer(0)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
stats.report()
|
|
||||||
timer.Reset(time.Second * 8)
|
|
||||||
case success := <-stop:
|
|
||||||
if success {
|
|
||||||
stats.reportDone()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateTrieRoot generates the trie hash based on the snapshot iterator.
|
|
||||||
// It can be used for generating account trie, storage trie or even the
|
|
||||||
// whole state which connects the accounts and the corresponding storages.
|
|
||||||
func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn trieGeneratorFn, leafCallback leafCallbackFn, stats *generateStats, report bool) (common.Hash, error) {
|
|
||||||
var (
|
|
||||||
in = make(chan trieKV) // chan to pass leaves
|
|
||||||
out = make(chan common.Hash, 1) // chan to collect result
|
|
||||||
stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled
|
|
||||||
wg sync.WaitGroup
|
|
||||||
)
|
|
||||||
// Spin up a go-routine for trie hash re-generation
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
generatorFn(db, scheme, account, in, out)
|
|
||||||
}()
|
|
||||||
// Spin up a go-routine for progress logging
|
|
||||||
if report && stats != nil {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
runReport(stats, stoplog)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// Create a semaphore to assign tasks and collect results through. We'll pre-
|
|
||||||
// fill it with nils, thus using the same channel for both limiting concurrent
|
|
||||||
// processing and gathering results.
|
|
||||||
threads := runtime.NumCPU()
|
|
||||||
results := make(chan error, threads)
|
|
||||||
for i := 0; i < threads; i++ {
|
|
||||||
results <- nil // fill the semaphore
|
|
||||||
}
|
|
||||||
// stop is a helper function to shutdown the background threads
|
|
||||||
// and return the re-generated trie hash.
|
|
||||||
stop := func(fail error) (common.Hash, error) {
|
|
||||||
close(in)
|
|
||||||
result := <-out
|
|
||||||
for i := 0; i < threads; i++ {
|
|
||||||
if err := <-results; err != nil && fail == nil {
|
|
||||||
fail = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stoplog <- fail == nil
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
return result, fail
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
logged = time.Now()
|
|
||||||
processed = uint64(0)
|
|
||||||
leaf trieKV
|
|
||||||
)
|
|
||||||
// Start to feed leaves
|
|
||||||
for it.Next() {
|
|
||||||
if account == (common.Hash{}) {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
fullData []byte
|
|
||||||
)
|
|
||||||
if leafCallback == nil {
|
|
||||||
fullData, err = types.FullAccountRLP(it.(AccountIterator).Account())
|
|
||||||
if err != nil {
|
|
||||||
return stop(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Wait until the semaphore allows us to continue, aborting if
|
|
||||||
// a sub-task failed
|
|
||||||
if err := <-results; err != nil {
|
|
||||||
results <- nil // stop will drain the results, add a noop back for this error we just consumed
|
|
||||||
return stop(err)
|
|
||||||
}
|
|
||||||
// Fetch the next account and process it concurrently
|
|
||||||
account, err := types.FullAccount(it.(AccountIterator).Account())
|
|
||||||
if err != nil {
|
|
||||||
return stop(err)
|
|
||||||
}
|
|
||||||
go func(hash common.Hash) {
|
|
||||||
subroot, err := leafCallback(db, hash, common.BytesToHash(account.CodeHash), stats)
|
|
||||||
if err != nil {
|
|
||||||
results <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if account.Root != subroot {
|
|
||||||
results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results <- nil
|
|
||||||
}(it.Hash())
|
|
||||||
fullData, err = rlp.EncodeToBytes(account)
|
|
||||||
if err != nil {
|
|
||||||
return stop(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
leaf = trieKV{it.Hash(), fullData}
|
|
||||||
} else {
|
|
||||||
leaf = trieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())}
|
|
||||||
}
|
|
||||||
in <- leaf
|
|
||||||
|
|
||||||
// Accumulate the generation statistic if it's required.
|
|
||||||
processed++
|
|
||||||
if time.Since(logged) > 3*time.Second && stats != nil {
|
|
||||||
if account == (common.Hash{}) {
|
|
||||||
stats.progressAccounts(it.Hash(), processed)
|
|
||||||
} else {
|
|
||||||
stats.progressContract(account, it.Hash(), processed)
|
|
||||||
}
|
|
||||||
logged, processed = time.Now(), 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Commit the last part statistic.
|
|
||||||
if processed > 0 && stats != nil {
|
|
||||||
if account == (common.Hash{}) {
|
|
||||||
stats.finishAccounts(processed)
|
|
||||||
} else {
|
|
||||||
stats.finishContract(account, processed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stop(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stackTrieGenerate(db ethdb.KeyValueWriter, scheme string, owner common.Hash, in chan trieKV, out chan common.Hash) {
|
|
||||||
options := trie.NewStackTrieOptions()
|
|
||||||
if db != nil {
|
|
||||||
options = options.WithWriter(func(path []byte, hash common.Hash, blob []byte) {
|
|
||||||
rawdb.WriteTrieNode(db, owner, path, hash, blob, scheme)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
t := trie.NewStackTrie(options)
|
|
||||||
for leaf := range in {
|
|
||||||
t.Update(leaf.key[:], leaf.value)
|
|
||||||
}
|
|
||||||
out <- t.Commit()
|
|
||||||
}
|
|
||||||
|
|
@ -1,570 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
bloomfilter "github.com/holiman/bloomfilter/v2"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// aggregatorMemoryLimit is the maximum size of the bottom-most diff layer
|
|
||||||
// that aggregates the writes from above until it's flushed into the disk
|
|
||||||
// layer.
|
|
||||||
//
|
|
||||||
// Note, bumping this up might drastically increase the size of the bloom
|
|
||||||
// filters that's stored in every diff layer. Don't do that without fully
|
|
||||||
// understanding all the implications.
|
|
||||||
aggregatorMemoryLimit = uint64(4 * 1024 * 1024)
|
|
||||||
|
|
||||||
// aggregatorItemLimit is an approximate number of items that will end up
|
|
||||||
// in the agregator layer before it's flushed out to disk. A plain account
|
|
||||||
// weighs around 14B (+hash), a storage slot 32B (+hash), a deleted slot
|
|
||||||
// 0B (+hash). Slots are mostly set/unset in lockstep, so that average at
|
|
||||||
// 16B (+hash). All in all, the average entry seems to be 15+32=47B. Use a
|
|
||||||
// smaller number to be on the safe side.
|
|
||||||
aggregatorItemLimit = aggregatorMemoryLimit / 42
|
|
||||||
|
|
||||||
// bloomTargetError is the target false positive rate when the aggregator
|
|
||||||
// layer is at its fullest. The actual value will probably move around up
|
|
||||||
// and down from this number, it's mostly a ballpark figure.
|
|
||||||
//
|
|
||||||
// Note, dropping this down might drastically increase the size of the bloom
|
|
||||||
// filters that's stored in every diff layer. Don't do that without fully
|
|
||||||
// understanding all the implications.
|
|
||||||
bloomTargetError = 0.02
|
|
||||||
|
|
||||||
// bloomSize is the ideal bloom filter size given the maximum number of items
|
|
||||||
// it's expected to hold and the target false positive error rate.
|
|
||||||
bloomSize = math.Ceil(float64(aggregatorItemLimit) * math.Log(bloomTargetError) / math.Log(1/math.Pow(2, math.Log(2))))
|
|
||||||
|
|
||||||
// bloomFuncs is the ideal number of bits a single entry should set in the
|
|
||||||
// bloom filter to keep its size to a minimum (given it's size and maximum
|
|
||||||
// entry count).
|
|
||||||
bloomFuncs = math.Round((bloomSize / float64(aggregatorItemLimit)) * math.Log(2))
|
|
||||||
|
|
||||||
// the bloom offsets are runtime constants which determines which part of the
|
|
||||||
// account/storage hash the hasher functions looks at, to determine the
|
|
||||||
// bloom key for an account/slot. This is randomized at init(), so that the
|
|
||||||
// global population of nodes do not all display the exact same behaviour with
|
|
||||||
// regards to bloom content
|
|
||||||
bloomDestructHasherOffset = 0
|
|
||||||
bloomAccountHasherOffset = 0
|
|
||||||
bloomStorageHasherOffset = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Init the bloom offsets in the range [0:24] (requires 8 bytes)
|
|
||||||
bloomDestructHasherOffset = rand.Intn(25)
|
|
||||||
bloomAccountHasherOffset = rand.Intn(25)
|
|
||||||
bloomStorageHasherOffset = rand.Intn(25)
|
|
||||||
|
|
||||||
// The destruct and account blooms must be different, as the storage slots
|
|
||||||
// will check for destruction too for every bloom miss. It should not collide
|
|
||||||
// with modified accounts.
|
|
||||||
for bloomAccountHasherOffset == bloomDestructHasherOffset {
|
|
||||||
bloomAccountHasherOffset = rand.Intn(25)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// diffLayer represents a collection of modifications made to a state snapshot
|
|
||||||
// after running a block on top. It contains one sorted list for the account trie
|
|
||||||
// and one-one list for each storage tries.
|
|
||||||
//
|
|
||||||
// The goal of a diff layer is to act as a journal, tracking recent modifications
|
|
||||||
// made to the state, that have not yet graduated into a semi-immutable state.
|
|
||||||
type diffLayer struct {
|
|
||||||
origin *diskLayer // Base disk layer to directly use on bloom misses
|
|
||||||
parent snapshot // Parent snapshot modified by this one, never nil
|
|
||||||
memory uint64 // Approximate guess as to how much memory we use
|
|
||||||
|
|
||||||
root common.Hash // Root hash to which this snapshot diff belongs to
|
|
||||||
stale atomic.Bool // Signals that the layer became stale (state progressed)
|
|
||||||
|
|
||||||
// destructSet is a very special helper marker. If an account is marked as
|
|
||||||
// deleted, then it's recorded in this set. However it's allowed that an account
|
|
||||||
// is included here but still available in other sets(e.g. storageData). The
|
|
||||||
// reason is the diff layer includes all the changes in a *block*. It can
|
|
||||||
// happen that in the tx_1, account A is self-destructed while in the tx_2
|
|
||||||
// it's recreated. But we still need this marker to indicate the "old" A is
|
|
||||||
// deleted, all data in other set belongs to the "new" A.
|
|
||||||
destructSet map[common.Hash]struct{} // Keyed markers for deleted (and potentially) recreated accounts
|
|
||||||
accountList []common.Hash // List of account for iteration. If it exists, it's sorted, otherwise it's nil
|
|
||||||
accountData map[common.Hash][]byte // Keyed accounts for direct retrieval (nil means deleted)
|
|
||||||
storageList map[common.Hash][]common.Hash // List of storage slots for iterated retrievals, one per account. Any existing lists are sorted if non-nil
|
|
||||||
storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrieval. one per account (nil means deleted)
|
|
||||||
|
|
||||||
diffed *bloomfilter.Filter // Bloom filter tracking all the diffed items up to the disk layer
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// destructBloomHasher is a wrapper around a common.Hash to satisfy the interface
|
|
||||||
// API requirements of the bloom library used. It's used to convert a destruct
|
|
||||||
// event into a 64 bit mini hash.
|
|
||||||
type destructBloomHasher common.Hash
|
|
||||||
|
|
||||||
func (h destructBloomHasher) Write(p []byte) (n int, err error) { panic("not implemented") }
|
|
||||||
func (h destructBloomHasher) Sum(b []byte) []byte { panic("not implemented") }
|
|
||||||
func (h destructBloomHasher) Reset() { panic("not implemented") }
|
|
||||||
func (h destructBloomHasher) BlockSize() int { panic("not implemented") }
|
|
||||||
func (h destructBloomHasher) Size() int { return 8 }
|
|
||||||
func (h destructBloomHasher) Sum64() uint64 {
|
|
||||||
return binary.BigEndian.Uint64(h[bloomDestructHasherOffset : bloomDestructHasherOffset+8])
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountBloomHasher is a wrapper around a common.Hash to satisfy the interface
|
|
||||||
// API requirements of the bloom library used. It's used to convert an account
|
|
||||||
// hash into a 64 bit mini hash.
|
|
||||||
type accountBloomHasher common.Hash
|
|
||||||
|
|
||||||
func (h accountBloomHasher) Write(p []byte) (n int, err error) { panic("not implemented") }
|
|
||||||
func (h accountBloomHasher) Sum(b []byte) []byte { panic("not implemented") }
|
|
||||||
func (h accountBloomHasher) Reset() { panic("not implemented") }
|
|
||||||
func (h accountBloomHasher) BlockSize() int { panic("not implemented") }
|
|
||||||
func (h accountBloomHasher) Size() int { return 8 }
|
|
||||||
func (h accountBloomHasher) Sum64() uint64 {
|
|
||||||
return binary.BigEndian.Uint64(h[bloomAccountHasherOffset : bloomAccountHasherOffset+8])
|
|
||||||
}
|
|
||||||
|
|
||||||
// storageBloomHasher is a wrapper around a [2]common.Hash to satisfy the interface
|
|
||||||
// API requirements of the bloom library used. It's used to convert an account
|
|
||||||
// hash into a 64 bit mini hash.
|
|
||||||
type storageBloomHasher [2]common.Hash
|
|
||||||
|
|
||||||
func (h storageBloomHasher) Write(p []byte) (n int, err error) { panic("not implemented") }
|
|
||||||
func (h storageBloomHasher) Sum(b []byte) []byte { panic("not implemented") }
|
|
||||||
func (h storageBloomHasher) Reset() { panic("not implemented") }
|
|
||||||
func (h storageBloomHasher) BlockSize() int { panic("not implemented") }
|
|
||||||
func (h storageBloomHasher) Size() int { return 8 }
|
|
||||||
func (h storageBloomHasher) Sum64() uint64 {
|
|
||||||
return binary.BigEndian.Uint64(h[0][bloomStorageHasherOffset:bloomStorageHasherOffset+8]) ^
|
|
||||||
binary.BigEndian.Uint64(h[1][bloomStorageHasherOffset:bloomStorageHasherOffset+8])
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDiffLayer creates a new diff on top of an existing snapshot, whether that's a low
|
|
||||||
// level persistent database or a hierarchical diff already.
|
|
||||||
func newDiffLayer(parent snapshot, root common.Hash, destructs map[common.Hash]struct{}, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer {
|
|
||||||
// Create the new layer with some pre-allocated data segments
|
|
||||||
dl := &diffLayer{
|
|
||||||
parent: parent,
|
|
||||||
root: root,
|
|
||||||
destructSet: destructs,
|
|
||||||
accountData: accounts,
|
|
||||||
storageData: storage,
|
|
||||||
storageList: make(map[common.Hash][]common.Hash),
|
|
||||||
}
|
|
||||||
switch parent := parent.(type) {
|
|
||||||
case *diskLayer:
|
|
||||||
dl.rebloom(parent)
|
|
||||||
case *diffLayer:
|
|
||||||
dl.rebloom(parent.origin)
|
|
||||||
default:
|
|
||||||
panic("unknown parent type")
|
|
||||||
}
|
|
||||||
// Sanity check that accounts or storage slots are never nil
|
|
||||||
for accountHash, blob := range accounts {
|
|
||||||
if blob == nil {
|
|
||||||
panic(fmt.Sprintf("account %#x nil", accountHash))
|
|
||||||
}
|
|
||||||
// Determine memory size and track the dirty writes
|
|
||||||
dl.memory += uint64(common.HashLength + len(blob))
|
|
||||||
snapshotDirtyAccountWriteMeter.Mark(int64(len(blob)))
|
|
||||||
}
|
|
||||||
for accountHash, slots := range storage {
|
|
||||||
if slots == nil {
|
|
||||||
panic(fmt.Sprintf("storage %#x nil", accountHash))
|
|
||||||
}
|
|
||||||
// Determine memory size and track the dirty writes
|
|
||||||
for _, data := range slots {
|
|
||||||
dl.memory += uint64(common.HashLength + len(data))
|
|
||||||
snapshotDirtyStorageWriteMeter.Mark(int64(len(data)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dl.memory += uint64(len(destructs) * common.HashLength)
|
|
||||||
return dl
|
|
||||||
}
|
|
||||||
|
|
||||||
// rebloom discards the layer's current bloom and rebuilds it from scratch based
|
|
||||||
// on the parent's and the local diffs.
|
|
||||||
func (dl *diffLayer) rebloom(origin *diskLayer) {
|
|
||||||
dl.lock.Lock()
|
|
||||||
defer dl.lock.Unlock()
|
|
||||||
|
|
||||||
defer func(start time.Time) {
|
|
||||||
snapshotBloomIndexTimer.Update(time.Since(start))
|
|
||||||
}(time.Now())
|
|
||||||
|
|
||||||
// Inject the new origin that triggered the rebloom
|
|
||||||
dl.origin = origin
|
|
||||||
|
|
||||||
// Retrieve the parent bloom or create a fresh empty one
|
|
||||||
if parent, ok := dl.parent.(*diffLayer); ok {
|
|
||||||
parent.lock.RLock()
|
|
||||||
dl.diffed, _ = parent.diffed.Copy()
|
|
||||||
parent.lock.RUnlock()
|
|
||||||
} else {
|
|
||||||
dl.diffed, _ = bloomfilter.New(uint64(bloomSize), uint64(bloomFuncs))
|
|
||||||
}
|
|
||||||
// Iterate over all the accounts and storage slots and index them
|
|
||||||
for hash := range dl.destructSet {
|
|
||||||
dl.diffed.Add(destructBloomHasher(hash))
|
|
||||||
}
|
|
||||||
for hash := range dl.accountData {
|
|
||||||
dl.diffed.Add(accountBloomHasher(hash))
|
|
||||||
}
|
|
||||||
for accountHash, slots := range dl.storageData {
|
|
||||||
for storageHash := range slots {
|
|
||||||
dl.diffed.Add(storageBloomHasher{accountHash, storageHash})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Calculate the current false positive rate and update the error rate meter.
|
|
||||||
// This is a bit cheating because subsequent layers will overwrite it, but it
|
|
||||||
// should be fine, we're only interested in ballpark figures.
|
|
||||||
k := float64(dl.diffed.K())
|
|
||||||
n := float64(dl.diffed.N())
|
|
||||||
m := float64(dl.diffed.M())
|
|
||||||
snapshotBloomErrorGauge.Update(math.Pow(1.0-math.Exp((-k)*(n+0.5)/(m-1)), k))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root returns the root hash for which this snapshot was made.
|
|
||||||
func (dl *diffLayer) Root() common.Hash {
|
|
||||||
return dl.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent returns the subsequent layer of a diff layer.
|
|
||||||
func (dl *diffLayer) Parent() snapshot {
|
|
||||||
dl.lock.RLock()
|
|
||||||
defer dl.lock.RUnlock()
|
|
||||||
|
|
||||||
return dl.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stale return whether this layer has become stale (was flattened across) or if
|
|
||||||
// it's still live.
|
|
||||||
func (dl *diffLayer) Stale() bool {
|
|
||||||
return dl.stale.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account directly retrieves the account associated with a particular hash in
|
|
||||||
// the snapshot slim data format.
|
|
||||||
func (dl *diffLayer) Account(hash common.Hash) (*types.SlimAccount, error) {
|
|
||||||
data, err := dl.AccountRLP(hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(data) == 0 { // can be both nil and []byte{}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
account := new(types.SlimAccount)
|
|
||||||
if err := rlp.DecodeBytes(data, account); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountRLP directly retrieves the account RLP associated with a particular
|
|
||||||
// hash in the snapshot slim data format.
|
|
||||||
//
|
|
||||||
// Note the returned account is not a copy, please don't modify it.
|
|
||||||
func (dl *diffLayer) AccountRLP(hash common.Hash) ([]byte, error) {
|
|
||||||
// Check staleness before reaching further.
|
|
||||||
dl.lock.RLock()
|
|
||||||
if dl.Stale() {
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
return nil, ErrSnapshotStale
|
|
||||||
}
|
|
||||||
// Check the bloom filter first whether there's even a point in reaching into
|
|
||||||
// all the maps in all the layers below
|
|
||||||
hit := dl.diffed.Contains(accountBloomHasher(hash))
|
|
||||||
if !hit {
|
|
||||||
hit = dl.diffed.Contains(destructBloomHasher(hash))
|
|
||||||
}
|
|
||||||
var origin *diskLayer
|
|
||||||
if !hit {
|
|
||||||
origin = dl.origin // extract origin while holding the lock
|
|
||||||
}
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// If the bloom filter misses, don't even bother with traversing the memory
|
|
||||||
// diff layers, reach straight into the bottom persistent disk layer
|
|
||||||
if origin != nil {
|
|
||||||
snapshotBloomAccountMissMeter.Mark(1)
|
|
||||||
return origin.AccountRLP(hash)
|
|
||||||
}
|
|
||||||
// The bloom filter hit, start poking in the internal maps
|
|
||||||
return dl.accountRLP(hash, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// accountRLP is an internal version of AccountRLP that skips the bloom filter
|
|
||||||
// checks and uses the internal maps to try and retrieve the data. It's meant
|
|
||||||
// to be used if a higher layer's bloom filter hit already.
|
|
||||||
func (dl *diffLayer) accountRLP(hash common.Hash, depth int) ([]byte, error) {
|
|
||||||
dl.lock.RLock()
|
|
||||||
defer dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// If the layer was flattened into, consider it invalid (any live reference to
|
|
||||||
// the original should be marked as unusable).
|
|
||||||
if dl.Stale() {
|
|
||||||
return nil, ErrSnapshotStale
|
|
||||||
}
|
|
||||||
// If the account is known locally, return it
|
|
||||||
if data, ok := dl.accountData[hash]; ok {
|
|
||||||
snapshotDirtyAccountHitMeter.Mark(1)
|
|
||||||
snapshotDirtyAccountHitDepthHist.Update(int64(depth))
|
|
||||||
snapshotDirtyAccountReadMeter.Mark(int64(len(data)))
|
|
||||||
snapshotBloomAccountTrueHitMeter.Mark(1)
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
// If the account is known locally, but deleted, return it
|
|
||||||
if _, ok := dl.destructSet[hash]; ok {
|
|
||||||
snapshotDirtyAccountHitMeter.Mark(1)
|
|
||||||
snapshotDirtyAccountHitDepthHist.Update(int64(depth))
|
|
||||||
snapshotDirtyAccountInexMeter.Mark(1)
|
|
||||||
snapshotBloomAccountTrueHitMeter.Mark(1)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
// Account unknown to this diff, resolve from parent
|
|
||||||
if diff, ok := dl.parent.(*diffLayer); ok {
|
|
||||||
return diff.accountRLP(hash, depth+1)
|
|
||||||
}
|
|
||||||
// Failed to resolve through diff layers, mark a bloom error and use the disk
|
|
||||||
snapshotBloomAccountFalseHitMeter.Mark(1)
|
|
||||||
return dl.parent.AccountRLP(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage directly retrieves the storage data associated with a particular hash,
|
|
||||||
// within a particular account. If the slot is unknown to this diff, it's parent
|
|
||||||
// is consulted.
|
|
||||||
//
|
|
||||||
// Note the returned slot is not a copy, please don't modify it.
|
|
||||||
func (dl *diffLayer) Storage(accountHash, storageHash common.Hash) ([]byte, error) {
|
|
||||||
// Check the bloom filter first whether there's even a point in reaching into
|
|
||||||
// all the maps in all the layers below
|
|
||||||
dl.lock.RLock()
|
|
||||||
// Check staleness before reaching further.
|
|
||||||
if dl.Stale() {
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
return nil, ErrSnapshotStale
|
|
||||||
}
|
|
||||||
hit := dl.diffed.Contains(storageBloomHasher{accountHash, storageHash})
|
|
||||||
if !hit {
|
|
||||||
hit = dl.diffed.Contains(destructBloomHasher(accountHash))
|
|
||||||
}
|
|
||||||
var origin *diskLayer
|
|
||||||
if !hit {
|
|
||||||
origin = dl.origin // extract origin while holding the lock
|
|
||||||
}
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// If the bloom filter misses, don't even bother with traversing the memory
|
|
||||||
// diff layers, reach straight into the bottom persistent disk layer
|
|
||||||
if origin != nil {
|
|
||||||
snapshotBloomStorageMissMeter.Mark(1)
|
|
||||||
return origin.Storage(accountHash, storageHash)
|
|
||||||
}
|
|
||||||
// The bloom filter hit, start poking in the internal maps
|
|
||||||
return dl.storage(accountHash, storageHash, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// storage is an internal version of Storage that skips the bloom filter checks
|
|
||||||
// and uses the internal maps to try and retrieve the data. It's meant to be
|
|
||||||
// used if a higher layer's bloom filter hit already.
|
|
||||||
func (dl *diffLayer) storage(accountHash, storageHash common.Hash, depth int) ([]byte, error) {
|
|
||||||
dl.lock.RLock()
|
|
||||||
defer dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// If the layer was flattened into, consider it invalid (any live reference to
|
|
||||||
// the original should be marked as unusable).
|
|
||||||
if dl.Stale() {
|
|
||||||
return nil, ErrSnapshotStale
|
|
||||||
}
|
|
||||||
// If the account is known locally, try to resolve the slot locally
|
|
||||||
if storage, ok := dl.storageData[accountHash]; ok {
|
|
||||||
if data, ok := storage[storageHash]; ok {
|
|
||||||
snapshotDirtyStorageHitMeter.Mark(1)
|
|
||||||
snapshotDirtyStorageHitDepthHist.Update(int64(depth))
|
|
||||||
if n := len(data); n > 0 {
|
|
||||||
snapshotDirtyStorageReadMeter.Mark(int64(n))
|
|
||||||
} else {
|
|
||||||
snapshotDirtyStorageInexMeter.Mark(1)
|
|
||||||
}
|
|
||||||
snapshotBloomStorageTrueHitMeter.Mark(1)
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the account is known locally, but deleted, return an empty slot
|
|
||||||
if _, ok := dl.destructSet[accountHash]; ok {
|
|
||||||
snapshotDirtyStorageHitMeter.Mark(1)
|
|
||||||
snapshotDirtyStorageHitDepthHist.Update(int64(depth))
|
|
||||||
snapshotDirtyStorageInexMeter.Mark(1)
|
|
||||||
snapshotBloomStorageTrueHitMeter.Mark(1)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
// Storage slot unknown to this diff, resolve from parent
|
|
||||||
if diff, ok := dl.parent.(*diffLayer); ok {
|
|
||||||
return diff.storage(accountHash, storageHash, depth+1)
|
|
||||||
}
|
|
||||||
// Failed to resolve through diff layers, mark a bloom error and use the disk
|
|
||||||
snapshotBloomStorageFalseHitMeter.Mark(1)
|
|
||||||
return dl.parent.Storage(accountHash, storageHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update creates a new layer on top of the existing snapshot diff tree with
|
|
||||||
// the specified data items.
|
|
||||||
func (dl *diffLayer) Update(blockRoot common.Hash, destructs map[common.Hash]struct{}, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer {
|
|
||||||
return newDiffLayer(dl, blockRoot, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// flatten pushes all data from this point downwards, flattening everything into
|
|
||||||
// a single diff at the bottom. Since usually the lowermost diff is the largest,
|
|
||||||
// the flattening builds up from there in reverse.
|
|
||||||
func (dl *diffLayer) flatten() snapshot {
|
|
||||||
// If the parent is not diff, we're the first in line, return unmodified
|
|
||||||
parent, ok := dl.parent.(*diffLayer)
|
|
||||||
if !ok {
|
|
||||||
return dl
|
|
||||||
}
|
|
||||||
// Parent is a diff, flatten it first (note, apart from weird corned cases,
|
|
||||||
// flatten will realistically only ever merge 1 layer, so there's no need to
|
|
||||||
// be smarter about grouping flattens together).
|
|
||||||
parent = parent.flatten().(*diffLayer)
|
|
||||||
|
|
||||||
parent.lock.Lock()
|
|
||||||
defer parent.lock.Unlock()
|
|
||||||
|
|
||||||
// Before actually writing all our data to the parent, first ensure that the
|
|
||||||
// parent hasn't been 'corrupted' by someone else already flattening into it
|
|
||||||
if parent.stale.Swap(true) {
|
|
||||||
panic("parent diff layer is stale") // we've flattened into the same parent from two children, boo
|
|
||||||
}
|
|
||||||
// Overwrite all the updated accounts blindly, merge the sorted list
|
|
||||||
for hash := range dl.destructSet {
|
|
||||||
parent.destructSet[hash] = struct{}{}
|
|
||||||
delete(parent.accountData, hash)
|
|
||||||
delete(parent.storageData, hash)
|
|
||||||
}
|
|
||||||
for hash, data := range dl.accountData {
|
|
||||||
parent.accountData[hash] = data
|
|
||||||
}
|
|
||||||
// Overwrite all the updated storage slots (individually)
|
|
||||||
for accountHash, storage := range dl.storageData {
|
|
||||||
// If storage didn't exist (or was deleted) in the parent, overwrite blindly
|
|
||||||
if _, ok := parent.storageData[accountHash]; !ok {
|
|
||||||
parent.storageData[accountHash] = storage
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Storage exists in both parent and child, merge the slots
|
|
||||||
comboData := parent.storageData[accountHash]
|
|
||||||
for storageHash, data := range storage {
|
|
||||||
comboData[storageHash] = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return the combo parent
|
|
||||||
return &diffLayer{
|
|
||||||
parent: parent.parent,
|
|
||||||
origin: parent.origin,
|
|
||||||
root: dl.root,
|
|
||||||
destructSet: parent.destructSet,
|
|
||||||
accountData: parent.accountData,
|
|
||||||
storageData: parent.storageData,
|
|
||||||
storageList: make(map[common.Hash][]common.Hash),
|
|
||||||
diffed: dl.diffed,
|
|
||||||
memory: parent.memory + dl.memory,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountList returns a sorted list of all accounts in this diffLayer, including
|
|
||||||
// the deleted ones.
|
|
||||||
//
|
|
||||||
// Note, the returned slice is not a copy, so do not modify it.
|
|
||||||
func (dl *diffLayer) AccountList() []common.Hash {
|
|
||||||
// If an old list already exists, return it
|
|
||||||
dl.lock.RLock()
|
|
||||||
list := dl.accountList
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
|
|
||||||
if list != nil {
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
// No old sorted account list exists, generate a new one
|
|
||||||
dl.lock.Lock()
|
|
||||||
defer dl.lock.Unlock()
|
|
||||||
|
|
||||||
dl.accountList = make([]common.Hash, 0, len(dl.destructSet)+len(dl.accountData))
|
|
||||||
for hash := range dl.accountData {
|
|
||||||
dl.accountList = append(dl.accountList, hash)
|
|
||||||
}
|
|
||||||
for hash := range dl.destructSet {
|
|
||||||
if _, ok := dl.accountData[hash]; !ok {
|
|
||||||
dl.accountList = append(dl.accountList, hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.SortFunc(dl.accountList, common.Hash.Cmp)
|
|
||||||
dl.memory += uint64(len(dl.accountList) * common.HashLength)
|
|
||||||
return dl.accountList
|
|
||||||
}
|
|
||||||
|
|
||||||
// StorageList returns a sorted list of all storage slot hashes in this diffLayer
|
|
||||||
// for the given account. If the whole storage is destructed in this layer, then
|
|
||||||
// an additional flag *destructed = true* will be returned, otherwise the flag is
|
|
||||||
// false. Besides, the returned list will include the hash of deleted storage slot.
|
|
||||||
// Note a special case is an account is deleted in a prior tx but is recreated in
|
|
||||||
// the following tx with some storage slots set. In this case the returned list is
|
|
||||||
// not empty but the flag is true.
|
|
||||||
//
|
|
||||||
// Note, the returned slice is not a copy, so do not modify it.
|
|
||||||
func (dl *diffLayer) StorageList(accountHash common.Hash) ([]common.Hash, bool) {
|
|
||||||
dl.lock.RLock()
|
|
||||||
_, destructed := dl.destructSet[accountHash]
|
|
||||||
if _, ok := dl.storageData[accountHash]; !ok {
|
|
||||||
// Account not tracked by this layer
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
return nil, destructed
|
|
||||||
}
|
|
||||||
// If an old list already exists, return it
|
|
||||||
if list, exist := dl.storageList[accountHash]; exist {
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
return list, destructed // the cached list can't be nil
|
|
||||||
}
|
|
||||||
dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// No old sorted account list exists, generate a new one
|
|
||||||
dl.lock.Lock()
|
|
||||||
defer dl.lock.Unlock()
|
|
||||||
|
|
||||||
storageMap := dl.storageData[accountHash]
|
|
||||||
storageList := make([]common.Hash, 0, len(storageMap))
|
|
||||||
for k := range storageMap {
|
|
||||||
storageList = append(storageList, k)
|
|
||||||
}
|
|
||||||
slices.SortFunc(storageList, common.Hash.Cmp)
|
|
||||||
dl.storageList[accountHash] = storageList
|
|
||||||
dl.memory += uint64(len(dl.storageList)*common.HashLength + common.HashLength)
|
|
||||||
return storageList, destructed
|
|
||||||
}
|
|
||||||
|
|
@ -1,399 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func copyDestructs(destructs map[common.Hash]struct{}) map[common.Hash]struct{} {
|
|
||||||
copy := make(map[common.Hash]struct{})
|
|
||||||
for hash := range destructs {
|
|
||||||
copy[hash] = struct{}{}
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyAccounts(accounts map[common.Hash][]byte) map[common.Hash][]byte {
|
|
||||||
copy := make(map[common.Hash][]byte)
|
|
||||||
for hash, blob := range accounts {
|
|
||||||
copy[hash] = blob
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyStorage(storage map[common.Hash]map[common.Hash][]byte) map[common.Hash]map[common.Hash][]byte {
|
|
||||||
copy := make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
for accHash, slots := range storage {
|
|
||||||
copy[accHash] = make(map[common.Hash][]byte)
|
|
||||||
for slotHash, blob := range slots {
|
|
||||||
copy[accHash][slotHash] = blob
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMergeBasics tests some simple merges
|
|
||||||
func TestMergeBasics(t *testing.T) {
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
// Fill up a parent
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
h := randomHash()
|
|
||||||
data := randomAccount()
|
|
||||||
|
|
||||||
accounts[h] = data
|
|
||||||
if rand.Intn(4) == 0 {
|
|
||||||
destructs[h] = struct{}{}
|
|
||||||
}
|
|
||||||
if rand.Intn(2) == 0 {
|
|
||||||
accStorage := make(map[common.Hash][]byte)
|
|
||||||
value := make([]byte, 32)
|
|
||||||
crand.Read(value)
|
|
||||||
accStorage[randomHash()] = value
|
|
||||||
storage[h] = accStorage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add some (identical) layers on top
|
|
||||||
parent := newDiffLayer(emptyLayer(), common.Hash{}, copyDestructs(destructs), copyAccounts(accounts), copyStorage(storage))
|
|
||||||
child := newDiffLayer(parent, common.Hash{}, copyDestructs(destructs), copyAccounts(accounts), copyStorage(storage))
|
|
||||||
child = newDiffLayer(child, common.Hash{}, copyDestructs(destructs), copyAccounts(accounts), copyStorage(storage))
|
|
||||||
child = newDiffLayer(child, common.Hash{}, copyDestructs(destructs), copyAccounts(accounts), copyStorage(storage))
|
|
||||||
child = newDiffLayer(child, common.Hash{}, copyDestructs(destructs), copyAccounts(accounts), copyStorage(storage))
|
|
||||||
// And flatten
|
|
||||||
merged := (child.flatten()).(*diffLayer)
|
|
||||||
|
|
||||||
{ // Check account lists
|
|
||||||
if have, want := len(merged.accountList), 0; have != want {
|
|
||||||
t.Errorf("accountList wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
if have, want := len(merged.AccountList()), len(accounts); have != want {
|
|
||||||
t.Errorf("AccountList() wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
if have, want := len(merged.accountList), len(accounts); have != want {
|
|
||||||
t.Errorf("accountList [2] wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{ // Check account drops
|
|
||||||
if have, want := len(merged.destructSet), len(destructs); have != want {
|
|
||||||
t.Errorf("accountDrop wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{ // Check storage lists
|
|
||||||
i := 0
|
|
||||||
for aHash, sMap := range storage {
|
|
||||||
if have, want := len(merged.storageList), i; have != want {
|
|
||||||
t.Errorf("[1] storageList wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
list, _ := merged.StorageList(aHash)
|
|
||||||
if have, want := len(list), len(sMap); have != want {
|
|
||||||
t.Errorf("[2] StorageList() wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
if have, want := len(merged.storageList[aHash]), len(sMap); have != want {
|
|
||||||
t.Errorf("storageList wrong: have %v, want %v", have, want)
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMergeDelete tests some deletion
|
|
||||||
func TestMergeDelete(t *testing.T) {
|
|
||||||
var (
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
// Fill up a parent
|
|
||||||
h1 := common.HexToHash("0x01")
|
|
||||||
h2 := common.HexToHash("0x02")
|
|
||||||
|
|
||||||
flipDrops := func() map[common.Hash]struct{} {
|
|
||||||
return map[common.Hash]struct{}{
|
|
||||||
h2: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flipAccs := func() map[common.Hash][]byte {
|
|
||||||
return map[common.Hash][]byte{
|
|
||||||
h1: randomAccount(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flopDrops := func() map[common.Hash]struct{} {
|
|
||||||
return map[common.Hash]struct{}{
|
|
||||||
h1: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flopAccs := func() map[common.Hash][]byte {
|
|
||||||
return map[common.Hash][]byte{
|
|
||||||
h2: randomAccount(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add some flipAccs-flopping layers on top
|
|
||||||
parent := newDiffLayer(emptyLayer(), common.Hash{}, flipDrops(), flipAccs(), storage)
|
|
||||||
child := parent.Update(common.Hash{}, flopDrops(), flopAccs(), storage)
|
|
||||||
child = child.Update(common.Hash{}, flipDrops(), flipAccs(), storage)
|
|
||||||
child = child.Update(common.Hash{}, flopDrops(), flopAccs(), storage)
|
|
||||||
child = child.Update(common.Hash{}, flipDrops(), flipAccs(), storage)
|
|
||||||
child = child.Update(common.Hash{}, flopDrops(), flopAccs(), storage)
|
|
||||||
child = child.Update(common.Hash{}, flipDrops(), flipAccs(), storage)
|
|
||||||
|
|
||||||
if data, _ := child.Account(h1); data == nil {
|
|
||||||
t.Errorf("last diff layer: expected %x account to be non-nil", h1)
|
|
||||||
}
|
|
||||||
if data, _ := child.Account(h2); data != nil {
|
|
||||||
t.Errorf("last diff layer: expected %x account to be nil", h2)
|
|
||||||
}
|
|
||||||
if _, ok := child.destructSet[h1]; ok {
|
|
||||||
t.Errorf("last diff layer: expected %x drop to be missing", h1)
|
|
||||||
}
|
|
||||||
if _, ok := child.destructSet[h2]; !ok {
|
|
||||||
t.Errorf("last diff layer: expected %x drop to be present", h1)
|
|
||||||
}
|
|
||||||
// And flatten
|
|
||||||
merged := (child.flatten()).(*diffLayer)
|
|
||||||
|
|
||||||
if data, _ := merged.Account(h1); data == nil {
|
|
||||||
t.Errorf("merged layer: expected %x account to be non-nil", h1)
|
|
||||||
}
|
|
||||||
if data, _ := merged.Account(h2); data != nil {
|
|
||||||
t.Errorf("merged layer: expected %x account to be nil", h2)
|
|
||||||
}
|
|
||||||
if _, ok := merged.destructSet[h1]; !ok { // Note, drops stay alive until persisted to disk!
|
|
||||||
t.Errorf("merged diff layer: expected %x drop to be present", h1)
|
|
||||||
}
|
|
||||||
if _, ok := merged.destructSet[h2]; !ok { // Note, drops stay alive until persisted to disk!
|
|
||||||
t.Errorf("merged diff layer: expected %x drop to be present", h1)
|
|
||||||
}
|
|
||||||
// If we add more granular metering of memory, we can enable this again,
|
|
||||||
// but it's not implemented for now
|
|
||||||
//if have, want := merged.memory, child.memory; have != want {
|
|
||||||
// t.Errorf("mem wrong: have %d, want %d", have, want)
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This tests that if we create a new account, and set a slot, and then merge
|
|
||||||
// it, the lists will be correct.
|
|
||||||
func TestInsertAndMerge(t *testing.T) {
|
|
||||||
// Fill up a parent
|
|
||||||
var (
|
|
||||||
acc = common.HexToHash("0x01")
|
|
||||||
slot = common.HexToHash("0x02")
|
|
||||||
parent *diffLayer
|
|
||||||
child *diffLayer
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
parent = newDiffLayer(emptyLayer(), common.Hash{}, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
accounts[acc] = randomAccount()
|
|
||||||
storage[acc] = make(map[common.Hash][]byte)
|
|
||||||
storage[acc][slot] = []byte{0x01}
|
|
||||||
child = newDiffLayer(parent, common.Hash{}, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
// And flatten
|
|
||||||
merged := (child.flatten()).(*diffLayer)
|
|
||||||
{ // Check that slot value is present
|
|
||||||
have, _ := merged.Storage(acc, slot)
|
|
||||||
if want := []byte{0x01}; !bytes.Equal(have, want) {
|
|
||||||
t.Errorf("merged slot value wrong: have %x, want %x", have, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func emptyLayer() *diskLayer {
|
|
||||||
return &diskLayer{
|
|
||||||
diskdb: memorydb.New(),
|
|
||||||
cache: fastcache.New(500 * 1024),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkSearch checks how long it takes to find a non-existing key
|
|
||||||
// BenchmarkSearch-6 200000 10481 ns/op (1K per layer)
|
|
||||||
// BenchmarkSearch-6 200000 10760 ns/op (10K per layer)
|
|
||||||
// BenchmarkSearch-6 100000 17866 ns/op
|
|
||||||
//
|
|
||||||
// BenchmarkSearch-6 500000 3723 ns/op (10k per layer, only top-level RLock()
|
|
||||||
func BenchmarkSearch(b *testing.B) {
|
|
||||||
// First, we set up 128 diff layers, with 1K items each
|
|
||||||
fill := func(parent snapshot) *diffLayer {
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
for i := 0; i < 10000; i++ {
|
|
||||||
accounts[randomHash()] = randomAccount()
|
|
||||||
}
|
|
||||||
return newDiffLayer(parent, common.Hash{}, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
var layer snapshot
|
|
||||||
layer = emptyLayer()
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
layer = fill(layer)
|
|
||||||
}
|
|
||||||
key := crypto.Keccak256Hash([]byte{0x13, 0x38})
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
layer.AccountRLP(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkSearchSlot checks how long it takes to find a non-existing key
|
|
||||||
// - Number of layers: 128
|
|
||||||
// - Each layers contains the account, with a couple of storage slots
|
|
||||||
// BenchmarkSearchSlot-6 100000 14554 ns/op
|
|
||||||
// BenchmarkSearchSlot-6 100000 22254 ns/op (when checking parent root using mutex)
|
|
||||||
// BenchmarkSearchSlot-6 100000 14551 ns/op (when checking parent number using atomic)
|
|
||||||
// With bloom filter:
|
|
||||||
// BenchmarkSearchSlot-6 3467835 351 ns/op
|
|
||||||
func BenchmarkSearchSlot(b *testing.B) {
|
|
||||||
// First, we set up 128 diff layers, with 1K items each
|
|
||||||
accountKey := crypto.Keccak256Hash([]byte{0x13, 0x37})
|
|
||||||
storageKey := crypto.Keccak256Hash([]byte{0x13, 0x37})
|
|
||||||
accountRLP := randomAccount()
|
|
||||||
fill := func(parent snapshot) *diffLayer {
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
accounts[accountKey] = accountRLP
|
|
||||||
|
|
||||||
accStorage := make(map[common.Hash][]byte)
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
value := make([]byte, 32)
|
|
||||||
crand.Read(value)
|
|
||||||
accStorage[randomHash()] = value
|
|
||||||
storage[accountKey] = accStorage
|
|
||||||
}
|
|
||||||
return newDiffLayer(parent, common.Hash{}, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
var layer snapshot
|
|
||||||
layer = emptyLayer()
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
layer = fill(layer)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
layer.Storage(accountKey, storageKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With accountList and sorting
|
|
||||||
// BenchmarkFlatten-6 50 29890856 ns/op
|
|
||||||
//
|
|
||||||
// Without sorting and tracking accountList
|
|
||||||
// BenchmarkFlatten-6 300 5511511 ns/op
|
|
||||||
func BenchmarkFlatten(b *testing.B) {
|
|
||||||
fill := func(parent snapshot) *diffLayer {
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
accountKey := randomHash()
|
|
||||||
accounts[accountKey] = randomAccount()
|
|
||||||
|
|
||||||
accStorage := make(map[common.Hash][]byte)
|
|
||||||
for i := 0; i < 20; i++ {
|
|
||||||
value := make([]byte, 32)
|
|
||||||
crand.Read(value)
|
|
||||||
accStorage[randomHash()] = value
|
|
||||||
}
|
|
||||||
storage[accountKey] = accStorage
|
|
||||||
}
|
|
||||||
return newDiffLayer(parent, common.Hash{}, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
b.StopTimer()
|
|
||||||
var layer snapshot
|
|
||||||
layer = emptyLayer()
|
|
||||||
for i := 1; i < 128; i++ {
|
|
||||||
layer = fill(layer)
|
|
||||||
}
|
|
||||||
b.StartTimer()
|
|
||||||
|
|
||||||
for i := 1; i < 128; i++ {
|
|
||||||
dl, ok := layer.(*diffLayer)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
layer = dl.flatten()
|
|
||||||
}
|
|
||||||
b.StopTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test writes ~324M of diff layers to disk, spread over
|
|
||||||
// - 128 individual layers,
|
|
||||||
// - each with 200 accounts
|
|
||||||
// - containing 200 slots
|
|
||||||
//
|
|
||||||
// BenchmarkJournal-6 1 1471373923 ns/ops
|
|
||||||
// BenchmarkJournal-6 1 1208083335 ns/op // bufio writer
|
|
||||||
func BenchmarkJournal(b *testing.B) {
|
|
||||||
fill := func(parent snapshot) *diffLayer {
|
|
||||||
var (
|
|
||||||
destructs = make(map[common.Hash]struct{})
|
|
||||||
accounts = make(map[common.Hash][]byte)
|
|
||||||
storage = make(map[common.Hash]map[common.Hash][]byte)
|
|
||||||
)
|
|
||||||
for i := 0; i < 200; i++ {
|
|
||||||
accountKey := randomHash()
|
|
||||||
accounts[accountKey] = randomAccount()
|
|
||||||
|
|
||||||
accStorage := make(map[common.Hash][]byte)
|
|
||||||
for i := 0; i < 200; i++ {
|
|
||||||
value := make([]byte, 32)
|
|
||||||
crand.Read(value)
|
|
||||||
accStorage[randomHash()] = value
|
|
||||||
}
|
|
||||||
storage[accountKey] = accStorage
|
|
||||||
}
|
|
||||||
return newDiffLayer(parent, common.Hash{}, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
layer := snapshot(emptyLayer())
|
|
||||||
for i := 1; i < 128; i++ {
|
|
||||||
layer = fill(layer)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
layer.Journal(new(bytes.Buffer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
)
|
|
||||||
|
|
||||||
// diskLayer is a low level persistent snapshot built on top of a key-value store.
|
|
||||||
type diskLayer struct {
|
|
||||||
diskdb ethdb.KeyValueStore // Key-value store containing the base snapshot
|
|
||||||
triedb *trie.Database // Trie node cache for reconstruction purposes
|
|
||||||
cache *fastcache.Cache // Cache to avoid hitting the disk for direct access
|
|
||||||
|
|
||||||
root common.Hash // Root hash of the base snapshot
|
|
||||||
stale bool // Signals that the layer became stale (state progressed)
|
|
||||||
|
|
||||||
genMarker []byte // Marker for the state that's indexed during initial layer generation
|
|
||||||
genPending chan struct{} // Notification channel when generation is done (test synchronicity)
|
|
||||||
genAbort chan chan *generatorStats // Notification channel to abort generating the snapshot in this layer
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release releases underlying resources; specifically the fastcache requires
|
|
||||||
// Reset() in order to not leak memory.
|
|
||||||
// OBS: It does not invoke Close on the diskdb
|
|
||||||
func (dl *diskLayer) Release() error {
|
|
||||||
if dl.cache != nil {
|
|
||||||
dl.cache.Reset()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root returns root hash for which this snapshot was made.
|
|
||||||
func (dl *diskLayer) Root() common.Hash {
|
|
||||||
return dl.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent always returns nil as there's no layer below the disk.
|
|
||||||
func (dl *diskLayer) Parent() snapshot {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stale return whether this layer has become stale (was flattened across) or if
|
|
||||||
// it's still live.
|
|
||||||
func (dl *diskLayer) Stale() bool {
|
|
||||||
dl.lock.RLock()
|
|
||||||
defer dl.lock.RUnlock()
|
|
||||||
|
|
||||||
return dl.stale
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account directly retrieves the account associated with a particular hash in
|
|
||||||
// the snapshot slim data format.
|
|
||||||
func (dl *diskLayer) Account(hash common.Hash) (*types.SlimAccount, error) {
|
|
||||||
data, err := dl.AccountRLP(hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(data) == 0 { // can be both nil and []byte{}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
account := new(types.SlimAccount)
|
|
||||||
if err := rlp.DecodeBytes(data, account); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountRLP directly retrieves the account RLP associated with a particular
|
|
||||||
// hash in the snapshot slim data format.
|
|
||||||
func (dl *diskLayer) AccountRLP(hash common.Hash) ([]byte, error) {
|
|
||||||
dl.lock.RLock()
|
|
||||||
defer dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// If the layer was flattened into, consider it invalid (any live reference to
|
|
||||||
// the original should be marked as unusable).
|
|
||||||
if dl.stale {
|
|
||||||
return nil, ErrSnapshotStale
|
|
||||||
}
|
|
||||||
// If the layer is being generated, ensure the requested hash has already been
|
|
||||||
// covered by the generator.
|
|
||||||
if dl.genMarker != nil && bytes.Compare(hash[:], dl.genMarker) > 0 {
|
|
||||||
return nil, ErrNotCoveredYet
|
|
||||||
}
|
|
||||||
// If we're in the disk layer, all diff layers missed
|
|
||||||
snapshotDirtyAccountMissMeter.Mark(1)
|
|
||||||
|
|
||||||
// Try to retrieve the account from the memory cache
|
|
||||||
if blob, found := dl.cache.HasGet(nil, hash[:]); found {
|
|
||||||
snapshotCleanAccountHitMeter.Mark(1)
|
|
||||||
snapshotCleanAccountReadMeter.Mark(int64(len(blob)))
|
|
||||||
return blob, nil
|
|
||||||
}
|
|
||||||
// Cache doesn't contain account, pull from disk and cache for later
|
|
||||||
blob := rawdb.ReadAccountSnapshot(dl.diskdb, hash)
|
|
||||||
dl.cache.Set(hash[:], blob)
|
|
||||||
|
|
||||||
snapshotCleanAccountMissMeter.Mark(1)
|
|
||||||
if n := len(blob); n > 0 {
|
|
||||||
snapshotCleanAccountWriteMeter.Mark(int64(n))
|
|
||||||
} else {
|
|
||||||
snapshotCleanAccountInexMeter.Mark(1)
|
|
||||||
}
|
|
||||||
return blob, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Storage directly retrieves the storage data associated with a particular hash,
|
|
||||||
// within a particular account.
|
|
||||||
func (dl *diskLayer) Storage(accountHash, storageHash common.Hash) ([]byte, error) {
|
|
||||||
dl.lock.RLock()
|
|
||||||
defer dl.lock.RUnlock()
|
|
||||||
|
|
||||||
// If the layer was flattened into, consider it invalid (any live reference to
|
|
||||||
// the original should be marked as unusable).
|
|
||||||
if dl.stale {
|
|
||||||
return nil, ErrSnapshotStale
|
|
||||||
}
|
|
||||||
key := append(accountHash[:], storageHash[:]...)
|
|
||||||
|
|
||||||
// If the layer is being generated, ensure the requested hash has already been
|
|
||||||
// covered by the generator.
|
|
||||||
if dl.genMarker != nil && bytes.Compare(key, dl.genMarker) > 0 {
|
|
||||||
return nil, ErrNotCoveredYet
|
|
||||||
}
|
|
||||||
// If we're in the disk layer, all diff layers missed
|
|
||||||
snapshotDirtyStorageMissMeter.Mark(1)
|
|
||||||
|
|
||||||
// Try to retrieve the storage slot from the memory cache
|
|
||||||
if blob, found := dl.cache.HasGet(nil, key); found {
|
|
||||||
snapshotCleanStorageHitMeter.Mark(1)
|
|
||||||
snapshotCleanStorageReadMeter.Mark(int64(len(blob)))
|
|
||||||
return blob, nil
|
|
||||||
}
|
|
||||||
// Cache doesn't contain storage slot, pull from disk and cache for later
|
|
||||||
blob := rawdb.ReadStorageSnapshot(dl.diskdb, accountHash, storageHash)
|
|
||||||
dl.cache.Set(key, blob)
|
|
||||||
|
|
||||||
snapshotCleanStorageMissMeter.Mark(1)
|
|
||||||
if n := len(blob); n > 0 {
|
|
||||||
snapshotCleanStorageWriteMeter.Mark(int64(n))
|
|
||||||
} else {
|
|
||||||
snapshotCleanStorageInexMeter.Mark(1)
|
|
||||||
}
|
|
||||||
return blob, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update creates a new layer on top of the existing snapshot diff tree with
|
|
||||||
// the specified data items. Note, the maps are retained by the method to avoid
|
|
||||||
// copying everything.
|
|
||||||
func (dl *diskLayer) Update(blockHash common.Hash, destructs map[common.Hash]struct{}, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer {
|
|
||||||
return newDiffLayer(dl, blockHash, destructs, accounts, storage)
|
|
||||||
}
|
|
||||||
|
|
@ -1,574 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb/memorydb"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// reverse reverses the contents of a byte slice. It's used to update random accs
|
|
||||||
// with deterministic changes.
|
|
||||||
func reverse(blob []byte) []byte {
|
|
||||||
res := make([]byte, len(blob))
|
|
||||||
for i, b := range blob {
|
|
||||||
res[len(blob)-1-i] = b
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that merging something into a disk layer persists it into the database
|
|
||||||
// and invalidates any previously written and cached values.
|
|
||||||
func TestDiskMerge(t *testing.T) {
|
|
||||||
// Create some accounts in the disk layer
|
|
||||||
db := memorydb.New()
|
|
||||||
|
|
||||||
var (
|
|
||||||
accNoModNoCache = common.Hash{0x1}
|
|
||||||
accNoModCache = common.Hash{0x2}
|
|
||||||
accModNoCache = common.Hash{0x3}
|
|
||||||
accModCache = common.Hash{0x4}
|
|
||||||
accDelNoCache = common.Hash{0x5}
|
|
||||||
accDelCache = common.Hash{0x6}
|
|
||||||
conNoModNoCache = common.Hash{0x7}
|
|
||||||
conNoModNoCacheSlot = common.Hash{0x70}
|
|
||||||
conNoModCache = common.Hash{0x8}
|
|
||||||
conNoModCacheSlot = common.Hash{0x80}
|
|
||||||
conModNoCache = common.Hash{0x9}
|
|
||||||
conModNoCacheSlot = common.Hash{0x90}
|
|
||||||
conModCache = common.Hash{0xa}
|
|
||||||
conModCacheSlot = common.Hash{0xa0}
|
|
||||||
conDelNoCache = common.Hash{0xb}
|
|
||||||
conDelNoCacheSlot = common.Hash{0xb0}
|
|
||||||
conDelCache = common.Hash{0xc}
|
|
||||||
conDelCacheSlot = common.Hash{0xc0}
|
|
||||||
conNukeNoCache = common.Hash{0xd}
|
|
||||||
conNukeNoCacheSlot = common.Hash{0xd0}
|
|
||||||
conNukeCache = common.Hash{0xe}
|
|
||||||
conNukeCacheSlot = common.Hash{0xe0}
|
|
||||||
baseRoot = randomHash()
|
|
||||||
diffRoot = randomHash()
|
|
||||||
)
|
|
||||||
|
|
||||||
rawdb.WriteAccountSnapshot(db, accNoModNoCache, accNoModNoCache[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, accNoModCache, accNoModCache[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, accModNoCache, accModNoCache[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, accModCache, accModCache[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, accDelNoCache, accDelNoCache[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, accDelCache, accDelCache[:])
|
|
||||||
|
|
||||||
rawdb.WriteAccountSnapshot(db, conNoModNoCache, conNoModNoCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conNoModNoCache, conNoModNoCacheSlot, conNoModNoCacheSlot[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, conNoModCache, conNoModCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, conModNoCache, conModNoCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conModNoCache, conModNoCacheSlot, conModNoCacheSlot[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, conModCache, conModCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conModCache, conModCacheSlot, conModCacheSlot[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, conDelNoCache, conDelNoCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conDelNoCache, conDelNoCacheSlot, conDelNoCacheSlot[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, conDelCache, conDelCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conDelCache, conDelCacheSlot, conDelCacheSlot[:])
|
|
||||||
|
|
||||||
rawdb.WriteAccountSnapshot(db, conNukeNoCache, conNukeNoCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conNukeNoCache, conNukeNoCacheSlot, conNukeNoCacheSlot[:])
|
|
||||||
rawdb.WriteAccountSnapshot(db, conNukeCache, conNukeCache[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, conNukeCache, conNukeCacheSlot, conNukeCacheSlot[:])
|
|
||||||
|
|
||||||
rawdb.WriteSnapshotRoot(db, baseRoot)
|
|
||||||
|
|
||||||
// Create a disk layer based on the above and cache in some data
|
|
||||||
snaps := &Tree{
|
|
||||||
layers: map[common.Hash]snapshot{
|
|
||||||
baseRoot: &diskLayer{
|
|
||||||
diskdb: db,
|
|
||||||
cache: fastcache.New(500 * 1024),
|
|
||||||
root: baseRoot,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
base := snaps.Snapshot(baseRoot)
|
|
||||||
base.AccountRLP(accNoModCache)
|
|
||||||
base.AccountRLP(accModCache)
|
|
||||||
base.AccountRLP(accDelCache)
|
|
||||||
base.Storage(conNoModCache, conNoModCacheSlot)
|
|
||||||
base.Storage(conModCache, conModCacheSlot)
|
|
||||||
base.Storage(conDelCache, conDelCacheSlot)
|
|
||||||
base.Storage(conNukeCache, conNukeCacheSlot)
|
|
||||||
|
|
||||||
// Modify or delete some accounts, flatten everything onto disk
|
|
||||||
if err := snaps.Update(diffRoot, baseRoot, map[common.Hash]struct{}{
|
|
||||||
accDelNoCache: {},
|
|
||||||
accDelCache: {},
|
|
||||||
conNukeNoCache: {},
|
|
||||||
conNukeCache: {},
|
|
||||||
}, map[common.Hash][]byte{
|
|
||||||
accModNoCache: reverse(accModNoCache[:]),
|
|
||||||
accModCache: reverse(accModCache[:]),
|
|
||||||
}, map[common.Hash]map[common.Hash][]byte{
|
|
||||||
conModNoCache: {conModNoCacheSlot: reverse(conModNoCacheSlot[:])},
|
|
||||||
conModCache: {conModCacheSlot: reverse(conModCacheSlot[:])},
|
|
||||||
conDelNoCache: {conDelNoCacheSlot: nil},
|
|
||||||
conDelCache: {conDelCacheSlot: nil},
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("failed to update snapshot tree: %v", err)
|
|
||||||
}
|
|
||||||
if err := snaps.Cap(diffRoot, 0); err != nil {
|
|
||||||
t.Fatalf("failed to flatten snapshot tree: %v", err)
|
|
||||||
}
|
|
||||||
// Retrieve all the data through the disk layer and validate it
|
|
||||||
base = snaps.Snapshot(diffRoot)
|
|
||||||
if _, ok := base.(*diskLayer); !ok {
|
|
||||||
t.Fatalf("update not flattend into the disk layer")
|
|
||||||
}
|
|
||||||
|
|
||||||
// assertAccount ensures that an account matches the given blob.
|
|
||||||
assertAccount := func(account common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
blob, err := base.AccountRLP(account)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("account access (%x) failed: %v", account, err)
|
|
||||||
} else if !bytes.Equal(blob, data) {
|
|
||||||
t.Errorf("account access (%x) mismatch: have %x, want %x", account, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertAccount(accNoModNoCache, accNoModNoCache[:])
|
|
||||||
assertAccount(accNoModCache, accNoModCache[:])
|
|
||||||
assertAccount(accModNoCache, reverse(accModNoCache[:]))
|
|
||||||
assertAccount(accModCache, reverse(accModCache[:]))
|
|
||||||
assertAccount(accDelNoCache, nil)
|
|
||||||
assertAccount(accDelCache, nil)
|
|
||||||
|
|
||||||
// assertStorage ensures that a storage slot matches the given blob.
|
|
||||||
assertStorage := func(account common.Hash, slot common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
blob, err := base.Storage(account, slot)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("storage access (%x:%x) failed: %v", account, slot, err)
|
|
||||||
} else if !bytes.Equal(blob, data) {
|
|
||||||
t.Errorf("storage access (%x:%x) mismatch: have %x, want %x", account, slot, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertStorage(conNoModNoCache, conNoModNoCacheSlot, conNoModNoCacheSlot[:])
|
|
||||||
assertStorage(conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
assertStorage(conModNoCache, conModNoCacheSlot, reverse(conModNoCacheSlot[:]))
|
|
||||||
assertStorage(conModCache, conModCacheSlot, reverse(conModCacheSlot[:]))
|
|
||||||
assertStorage(conDelNoCache, conDelNoCacheSlot, nil)
|
|
||||||
assertStorage(conDelCache, conDelCacheSlot, nil)
|
|
||||||
assertStorage(conNukeNoCache, conNukeNoCacheSlot, nil)
|
|
||||||
assertStorage(conNukeCache, conNukeCacheSlot, nil)
|
|
||||||
|
|
||||||
// Retrieve all the data directly from the database and validate it
|
|
||||||
|
|
||||||
// assertDatabaseAccount ensures that an account from the database matches the given blob.
|
|
||||||
assertDatabaseAccount := func(account common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
if blob := rawdb.ReadAccountSnapshot(db, account); !bytes.Equal(blob, data) {
|
|
||||||
t.Errorf("account database access (%x) mismatch: have %x, want %x", account, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertDatabaseAccount(accNoModNoCache, accNoModNoCache[:])
|
|
||||||
assertDatabaseAccount(accNoModCache, accNoModCache[:])
|
|
||||||
assertDatabaseAccount(accModNoCache, reverse(accModNoCache[:]))
|
|
||||||
assertDatabaseAccount(accModCache, reverse(accModCache[:]))
|
|
||||||
assertDatabaseAccount(accDelNoCache, nil)
|
|
||||||
assertDatabaseAccount(accDelCache, nil)
|
|
||||||
|
|
||||||
// assertDatabaseStorage ensures that a storage slot from the database matches the given blob.
|
|
||||||
assertDatabaseStorage := func(account common.Hash, slot common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
if blob := rawdb.ReadStorageSnapshot(db, account, slot); !bytes.Equal(blob, data) {
|
|
||||||
t.Errorf("storage database access (%x:%x) mismatch: have %x, want %x", account, slot, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertDatabaseStorage(conNoModNoCache, conNoModNoCacheSlot, conNoModNoCacheSlot[:])
|
|
||||||
assertDatabaseStorage(conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
assertDatabaseStorage(conModNoCache, conModNoCacheSlot, reverse(conModNoCacheSlot[:]))
|
|
||||||
assertDatabaseStorage(conModCache, conModCacheSlot, reverse(conModCacheSlot[:]))
|
|
||||||
assertDatabaseStorage(conDelNoCache, conDelNoCacheSlot, nil)
|
|
||||||
assertDatabaseStorage(conDelCache, conDelCacheSlot, nil)
|
|
||||||
assertDatabaseStorage(conNukeNoCache, conNukeNoCacheSlot, nil)
|
|
||||||
assertDatabaseStorage(conNukeCache, conNukeCacheSlot, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that merging something into a disk layer persists it into the database
|
|
||||||
// and invalidates any previously written and cached values, discarding anything
|
|
||||||
// after the in-progress generation marker.
|
|
||||||
func TestDiskPartialMerge(t *testing.T) {
|
|
||||||
// Iterate the test a few times to ensure we pick various internal orderings
|
|
||||||
// for the data slots as well as the progress marker.
|
|
||||||
for i := 0; i < 1024; i++ {
|
|
||||||
// Create some accounts in the disk layer
|
|
||||||
db := memorydb.New()
|
|
||||||
|
|
||||||
var (
|
|
||||||
accNoModNoCache = randomHash()
|
|
||||||
accNoModCache = randomHash()
|
|
||||||
accModNoCache = randomHash()
|
|
||||||
accModCache = randomHash()
|
|
||||||
accDelNoCache = randomHash()
|
|
||||||
accDelCache = randomHash()
|
|
||||||
conNoModNoCache = randomHash()
|
|
||||||
conNoModNoCacheSlot = randomHash()
|
|
||||||
conNoModCache = randomHash()
|
|
||||||
conNoModCacheSlot = randomHash()
|
|
||||||
conModNoCache = randomHash()
|
|
||||||
conModNoCacheSlot = randomHash()
|
|
||||||
conModCache = randomHash()
|
|
||||||
conModCacheSlot = randomHash()
|
|
||||||
conDelNoCache = randomHash()
|
|
||||||
conDelNoCacheSlot = randomHash()
|
|
||||||
conDelCache = randomHash()
|
|
||||||
conDelCacheSlot = randomHash()
|
|
||||||
conNukeNoCache = randomHash()
|
|
||||||
conNukeNoCacheSlot = randomHash()
|
|
||||||
conNukeCache = randomHash()
|
|
||||||
conNukeCacheSlot = randomHash()
|
|
||||||
baseRoot = randomHash()
|
|
||||||
diffRoot = randomHash()
|
|
||||||
genMarker = append(randomHash().Bytes(), randomHash().Bytes()...)
|
|
||||||
)
|
|
||||||
|
|
||||||
// insertAccount injects an account into the database if it's after the
|
|
||||||
// generator marker, drops the op otherwise. This is needed to seed the
|
|
||||||
// database with a valid starting snapshot.
|
|
||||||
insertAccount := func(account common.Hash, data []byte) {
|
|
||||||
if bytes.Compare(account[:], genMarker) <= 0 {
|
|
||||||
rawdb.WriteAccountSnapshot(db, account, data[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertAccount(accNoModNoCache, accNoModNoCache[:])
|
|
||||||
insertAccount(accNoModCache, accNoModCache[:])
|
|
||||||
insertAccount(accModNoCache, accModNoCache[:])
|
|
||||||
insertAccount(accModCache, accModCache[:])
|
|
||||||
insertAccount(accDelNoCache, accDelNoCache[:])
|
|
||||||
insertAccount(accDelCache, accDelCache[:])
|
|
||||||
|
|
||||||
// insertStorage injects a storage slot into the database if it's after
|
|
||||||
// the generator marker, drops the op otherwise. This is needed to seed
|
|
||||||
// the database with a valid starting snapshot.
|
|
||||||
insertStorage := func(account common.Hash, slot common.Hash, data []byte) {
|
|
||||||
if bytes.Compare(append(account[:], slot[:]...), genMarker) <= 0 {
|
|
||||||
rawdb.WriteStorageSnapshot(db, account, slot, data[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertAccount(conNoModNoCache, conNoModNoCache[:])
|
|
||||||
insertStorage(conNoModNoCache, conNoModNoCacheSlot, conNoModNoCacheSlot[:])
|
|
||||||
insertAccount(conNoModCache, conNoModCache[:])
|
|
||||||
insertStorage(conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
insertAccount(conModNoCache, conModNoCache[:])
|
|
||||||
insertStorage(conModNoCache, conModNoCacheSlot, conModNoCacheSlot[:])
|
|
||||||
insertAccount(conModCache, conModCache[:])
|
|
||||||
insertStorage(conModCache, conModCacheSlot, conModCacheSlot[:])
|
|
||||||
insertAccount(conDelNoCache, conDelNoCache[:])
|
|
||||||
insertStorage(conDelNoCache, conDelNoCacheSlot, conDelNoCacheSlot[:])
|
|
||||||
insertAccount(conDelCache, conDelCache[:])
|
|
||||||
insertStorage(conDelCache, conDelCacheSlot, conDelCacheSlot[:])
|
|
||||||
|
|
||||||
insertAccount(conNukeNoCache, conNukeNoCache[:])
|
|
||||||
insertStorage(conNukeNoCache, conNukeNoCacheSlot, conNukeNoCacheSlot[:])
|
|
||||||
insertAccount(conNukeCache, conNukeCache[:])
|
|
||||||
insertStorage(conNukeCache, conNukeCacheSlot, conNukeCacheSlot[:])
|
|
||||||
|
|
||||||
rawdb.WriteSnapshotRoot(db, baseRoot)
|
|
||||||
|
|
||||||
// Create a disk layer based on the above using a random progress marker
|
|
||||||
// and cache in some data.
|
|
||||||
snaps := &Tree{
|
|
||||||
layers: map[common.Hash]snapshot{
|
|
||||||
baseRoot: &diskLayer{
|
|
||||||
diskdb: db,
|
|
||||||
cache: fastcache.New(500 * 1024),
|
|
||||||
root: baseRoot,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
snaps.layers[baseRoot].(*diskLayer).genMarker = genMarker
|
|
||||||
base := snaps.Snapshot(baseRoot)
|
|
||||||
|
|
||||||
// assertAccount ensures that an account matches the given blob if it's
|
|
||||||
// already covered by the disk snapshot, and errors out otherwise.
|
|
||||||
assertAccount := func(account common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
blob, err := base.AccountRLP(account)
|
|
||||||
if bytes.Compare(account[:], genMarker) > 0 && err != ErrNotCoveredYet {
|
|
||||||
t.Fatalf("test %d: post-marker (%x) account access (%x) succeeded: %x", i, genMarker, account, blob)
|
|
||||||
}
|
|
||||||
if bytes.Compare(account[:], genMarker) <= 0 && !bytes.Equal(blob, data) {
|
|
||||||
t.Fatalf("test %d: pre-marker (%x) account access (%x) mismatch: have %x, want %x", i, genMarker, account, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertAccount(accNoModCache, accNoModCache[:])
|
|
||||||
assertAccount(accModCache, accModCache[:])
|
|
||||||
assertAccount(accDelCache, accDelCache[:])
|
|
||||||
|
|
||||||
// assertStorage ensures that a storage slot matches the given blob if
|
|
||||||
// it's already covered by the disk snapshot, and errors out otherwise.
|
|
||||||
assertStorage := func(account common.Hash, slot common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
blob, err := base.Storage(account, slot)
|
|
||||||
if bytes.Compare(append(account[:], slot[:]...), genMarker) > 0 && err != ErrNotCoveredYet {
|
|
||||||
t.Fatalf("test %d: post-marker (%x) storage access (%x:%x) succeeded: %x", i, genMarker, account, slot, blob)
|
|
||||||
}
|
|
||||||
if bytes.Compare(append(account[:], slot[:]...), genMarker) <= 0 && !bytes.Equal(blob, data) {
|
|
||||||
t.Fatalf("test %d: pre-marker (%x) storage access (%x:%x) mismatch: have %x, want %x", i, genMarker, account, slot, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertStorage(conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
assertStorage(conModCache, conModCacheSlot, conModCacheSlot[:])
|
|
||||||
assertStorage(conDelCache, conDelCacheSlot, conDelCacheSlot[:])
|
|
||||||
assertStorage(conNukeCache, conNukeCacheSlot, conNukeCacheSlot[:])
|
|
||||||
|
|
||||||
// Modify or delete some accounts, flatten everything onto disk
|
|
||||||
if err := snaps.Update(diffRoot, baseRoot, map[common.Hash]struct{}{
|
|
||||||
accDelNoCache: {},
|
|
||||||
accDelCache: {},
|
|
||||||
conNukeNoCache: {},
|
|
||||||
conNukeCache: {},
|
|
||||||
}, map[common.Hash][]byte{
|
|
||||||
accModNoCache: reverse(accModNoCache[:]),
|
|
||||||
accModCache: reverse(accModCache[:]),
|
|
||||||
}, map[common.Hash]map[common.Hash][]byte{
|
|
||||||
conModNoCache: {conModNoCacheSlot: reverse(conModNoCacheSlot[:])},
|
|
||||||
conModCache: {conModCacheSlot: reverse(conModCacheSlot[:])},
|
|
||||||
conDelNoCache: {conDelNoCacheSlot: nil},
|
|
||||||
conDelCache: {conDelCacheSlot: nil},
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("test %d: failed to update snapshot tree: %v", i, err)
|
|
||||||
}
|
|
||||||
if err := snaps.Cap(diffRoot, 0); err != nil {
|
|
||||||
t.Fatalf("test %d: failed to flatten snapshot tree: %v", i, err)
|
|
||||||
}
|
|
||||||
// Retrieve all the data through the disk layer and validate it
|
|
||||||
base = snaps.Snapshot(diffRoot)
|
|
||||||
if _, ok := base.(*diskLayer); !ok {
|
|
||||||
t.Fatalf("test %d: update not flattend into the disk layer", i)
|
|
||||||
}
|
|
||||||
assertAccount(accNoModNoCache, accNoModNoCache[:])
|
|
||||||
assertAccount(accNoModCache, accNoModCache[:])
|
|
||||||
assertAccount(accModNoCache, reverse(accModNoCache[:]))
|
|
||||||
assertAccount(accModCache, reverse(accModCache[:]))
|
|
||||||
assertAccount(accDelNoCache, nil)
|
|
||||||
assertAccount(accDelCache, nil)
|
|
||||||
|
|
||||||
assertStorage(conNoModNoCache, conNoModNoCacheSlot, conNoModNoCacheSlot[:])
|
|
||||||
assertStorage(conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
assertStorage(conModNoCache, conModNoCacheSlot, reverse(conModNoCacheSlot[:]))
|
|
||||||
assertStorage(conModCache, conModCacheSlot, reverse(conModCacheSlot[:]))
|
|
||||||
assertStorage(conDelNoCache, conDelNoCacheSlot, nil)
|
|
||||||
assertStorage(conDelCache, conDelCacheSlot, nil)
|
|
||||||
assertStorage(conNukeNoCache, conNukeNoCacheSlot, nil)
|
|
||||||
assertStorage(conNukeCache, conNukeCacheSlot, nil)
|
|
||||||
|
|
||||||
// Retrieve all the data directly from the database and validate it
|
|
||||||
|
|
||||||
// assertDatabaseAccount ensures that an account inside the database matches
|
|
||||||
// the given blob if it's already covered by the disk snapshot, and does not
|
|
||||||
// exist otherwise.
|
|
||||||
assertDatabaseAccount := func(account common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
blob := rawdb.ReadAccountSnapshot(db, account)
|
|
||||||
if bytes.Compare(account[:], genMarker) > 0 && blob != nil {
|
|
||||||
t.Fatalf("test %d: post-marker (%x) account database access (%x) succeeded: %x", i, genMarker, account, blob)
|
|
||||||
}
|
|
||||||
if bytes.Compare(account[:], genMarker) <= 0 && !bytes.Equal(blob, data) {
|
|
||||||
t.Fatalf("test %d: pre-marker (%x) account database access (%x) mismatch: have %x, want %x", i, genMarker, account, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertDatabaseAccount(accNoModNoCache, accNoModNoCache[:])
|
|
||||||
assertDatabaseAccount(accNoModCache, accNoModCache[:])
|
|
||||||
assertDatabaseAccount(accModNoCache, reverse(accModNoCache[:]))
|
|
||||||
assertDatabaseAccount(accModCache, reverse(accModCache[:]))
|
|
||||||
assertDatabaseAccount(accDelNoCache, nil)
|
|
||||||
assertDatabaseAccount(accDelCache, nil)
|
|
||||||
|
|
||||||
// assertDatabaseStorage ensures that a storage slot inside the database
|
|
||||||
// matches the given blob if it's already covered by the disk snapshot,
|
|
||||||
// and does not exist otherwise.
|
|
||||||
assertDatabaseStorage := func(account common.Hash, slot common.Hash, data []byte) {
|
|
||||||
t.Helper()
|
|
||||||
blob := rawdb.ReadStorageSnapshot(db, account, slot)
|
|
||||||
if bytes.Compare(append(account[:], slot[:]...), genMarker) > 0 && blob != nil {
|
|
||||||
t.Fatalf("test %d: post-marker (%x) storage database access (%x:%x) succeeded: %x", i, genMarker, account, slot, blob)
|
|
||||||
}
|
|
||||||
if bytes.Compare(append(account[:], slot[:]...), genMarker) <= 0 && !bytes.Equal(blob, data) {
|
|
||||||
t.Fatalf("test %d: pre-marker (%x) storage database access (%x:%x) mismatch: have %x, want %x", i, genMarker, account, slot, blob, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertDatabaseStorage(conNoModNoCache, conNoModNoCacheSlot, conNoModNoCacheSlot[:])
|
|
||||||
assertDatabaseStorage(conNoModCache, conNoModCacheSlot, conNoModCacheSlot[:])
|
|
||||||
assertDatabaseStorage(conModNoCache, conModNoCacheSlot, reverse(conModNoCacheSlot[:]))
|
|
||||||
assertDatabaseStorage(conModCache, conModCacheSlot, reverse(conModCacheSlot[:]))
|
|
||||||
assertDatabaseStorage(conDelNoCache, conDelNoCacheSlot, nil)
|
|
||||||
assertDatabaseStorage(conDelCache, conDelCacheSlot, nil)
|
|
||||||
assertDatabaseStorage(conNukeNoCache, conNukeNoCacheSlot, nil)
|
|
||||||
assertDatabaseStorage(conNukeCache, conNukeCacheSlot, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that when the bottom-most diff layer is merged into the disk
|
|
||||||
// layer whether the corresponding generator is persisted correctly.
|
|
||||||
func TestDiskGeneratorPersistence(t *testing.T) {
|
|
||||||
var (
|
|
||||||
accOne = randomHash()
|
|
||||||
accTwo = randomHash()
|
|
||||||
accOneSlotOne = randomHash()
|
|
||||||
accOneSlotTwo = randomHash()
|
|
||||||
|
|
||||||
accThree = randomHash()
|
|
||||||
accThreeSlot = randomHash()
|
|
||||||
baseRoot = randomHash()
|
|
||||||
diffRoot = randomHash()
|
|
||||||
diffTwoRoot = randomHash()
|
|
||||||
genMarker = append(randomHash().Bytes(), randomHash().Bytes()...)
|
|
||||||
)
|
|
||||||
// Testing scenario 1, the disk layer is still under the construction.
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
|
|
||||||
rawdb.WriteAccountSnapshot(db, accOne, accOne[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, accOne, accOneSlotOne, accOneSlotOne[:])
|
|
||||||
rawdb.WriteStorageSnapshot(db, accOne, accOneSlotTwo, accOneSlotTwo[:])
|
|
||||||
rawdb.WriteSnapshotRoot(db, baseRoot)
|
|
||||||
|
|
||||||
// Create a disk layer based on all above updates
|
|
||||||
snaps := &Tree{
|
|
||||||
layers: map[common.Hash]snapshot{
|
|
||||||
baseRoot: &diskLayer{
|
|
||||||
diskdb: db,
|
|
||||||
cache: fastcache.New(500 * 1024),
|
|
||||||
root: baseRoot,
|
|
||||||
genMarker: genMarker,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Modify or delete some accounts, flatten everything onto disk
|
|
||||||
if err := snaps.Update(diffRoot, baseRoot, nil, map[common.Hash][]byte{
|
|
||||||
accTwo: accTwo[:],
|
|
||||||
}, nil); err != nil {
|
|
||||||
t.Fatalf("failed to update snapshot tree: %v", err)
|
|
||||||
}
|
|
||||||
if err := snaps.Cap(diffRoot, 0); err != nil {
|
|
||||||
t.Fatalf("failed to flatten snapshot tree: %v", err)
|
|
||||||
}
|
|
||||||
blob := rawdb.ReadSnapshotGenerator(db)
|
|
||||||
var generator journalGenerator
|
|
||||||
if err := rlp.DecodeBytes(blob, &generator); err != nil {
|
|
||||||
t.Fatalf("Failed to decode snapshot generator %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(generator.Marker, genMarker) {
|
|
||||||
t.Fatalf("Generator marker is not matched")
|
|
||||||
}
|
|
||||||
// Test scenario 2, the disk layer is fully generated
|
|
||||||
// Modify or delete some accounts, flatten everything onto disk
|
|
||||||
if err := snaps.Update(diffTwoRoot, diffRoot, nil, map[common.Hash][]byte{
|
|
||||||
accThree: accThree.Bytes(),
|
|
||||||
}, map[common.Hash]map[common.Hash][]byte{
|
|
||||||
accThree: {accThreeSlot: accThreeSlot.Bytes()},
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("failed to update snapshot tree: %v", err)
|
|
||||||
}
|
|
||||||
diskLayer := snaps.layers[snaps.diskRoot()].(*diskLayer)
|
|
||||||
diskLayer.genMarker = nil // Construction finished
|
|
||||||
if err := snaps.Cap(diffTwoRoot, 0); err != nil {
|
|
||||||
t.Fatalf("failed to flatten snapshot tree: %v", err)
|
|
||||||
}
|
|
||||||
blob = rawdb.ReadSnapshotGenerator(db)
|
|
||||||
if err := rlp.DecodeBytes(blob, &generator); err != nil {
|
|
||||||
t.Fatalf("Failed to decode snapshot generator %v", err)
|
|
||||||
}
|
|
||||||
if len(generator.Marker) != 0 {
|
|
||||||
t.Fatalf("Failed to update snapshot generator")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that merging something into a disk layer persists it into the database
|
|
||||||
// and invalidates any previously written and cached values, discarding anything
|
|
||||||
// after the in-progress generation marker.
|
|
||||||
//
|
|
||||||
// This test case is a tiny specialized case of TestDiskPartialMerge, which tests
|
|
||||||
// some very specific cornercases that random tests won't ever trigger.
|
|
||||||
func TestDiskMidAccountPartialMerge(t *testing.T) {
|
|
||||||
// TODO(@karalabe) ?
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDiskSeek tests that seek-operations work on the disk layer
|
|
||||||
func TestDiskSeek(t *testing.T) {
|
|
||||||
// Create some accounts in the disk layer
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Fill even keys [0,2,4...]
|
|
||||||
for i := 0; i < 0xff; i += 2 {
|
|
||||||
acc := common.Hash{byte(i)}
|
|
||||||
rawdb.WriteAccountSnapshot(db, acc, acc[:])
|
|
||||||
}
|
|
||||||
// Add an 'higher' key, with incorrect (higher) prefix
|
|
||||||
highKey := []byte{rawdb.SnapshotAccountPrefix[0] + 1}
|
|
||||||
db.Put(highKey, []byte{0xff, 0xff})
|
|
||||||
|
|
||||||
baseRoot := randomHash()
|
|
||||||
rawdb.WriteSnapshotRoot(db, baseRoot)
|
|
||||||
|
|
||||||
snaps := &Tree{
|
|
||||||
layers: map[common.Hash]snapshot{
|
|
||||||
baseRoot: &diskLayer{
|
|
||||||
diskdb: db,
|
|
||||||
cache: fastcache.New(500 * 1024),
|
|
||||||
root: baseRoot,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Test some different seek positions
|
|
||||||
type testcase struct {
|
|
||||||
pos byte
|
|
||||||
expkey byte
|
|
||||||
}
|
|
||||||
var cases = []testcase{
|
|
||||||
{0xff, 0x55}, // this should exit immediately without checking key
|
|
||||||
{0x01, 0x02},
|
|
||||||
{0xfe, 0xfe},
|
|
||||||
{0xfd, 0xfe},
|
|
||||||
{0x00, 0x00},
|
|
||||||
}
|
|
||||||
for i, tc := range cases {
|
|
||||||
it, err := snaps.AccountIterator(baseRoot, common.Hash{tc.pos})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("case %d, error: %v", i, err)
|
|
||||||
}
|
|
||||||
count := 0
|
|
||||||
for it.Next() {
|
|
||||||
k, v, err := it.Hash()[0], it.Account()[0], it.Error()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d, item %d, error: %v", i, count, err)
|
|
||||||
}
|
|
||||||
// First item in iterator should have the expected key
|
|
||||||
if count == 0 && k != tc.expkey {
|
|
||||||
t.Fatalf("test %d, item %d, got %v exp %v", i, count, k, tc.expkey)
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
if v != k {
|
|
||||||
t.Fatalf("test %d, item %d, value wrong, got %v exp %v", i, count, v, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,749 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// accountCheckRange is the upper limit of the number of accounts involved in
|
|
||||||
// each range check. This is a value estimated based on experience. If this
|
|
||||||
// range is too large, the failure rate of range proof will increase. Otherwise,
|
|
||||||
// if the range is too small, the efficiency of the state recovery will decrease.
|
|
||||||
accountCheckRange = 128
|
|
||||||
|
|
||||||
// storageCheckRange is the upper limit of the number of storage slots involved
|
|
||||||
// in each range check. This is a value estimated based on experience. If this
|
|
||||||
// range is too large, the failure rate of range proof will increase. Otherwise,
|
|
||||||
// if the range is too small, the efficiency of the state recovery will decrease.
|
|
||||||
storageCheckRange = 1024
|
|
||||||
|
|
||||||
// errMissingTrie is returned if the target trie is missing while the generation
|
|
||||||
// is running. In this case the generation is aborted and wait the new signal.
|
|
||||||
errMissingTrie = errors.New("missing trie")
|
|
||||||
)
|
|
||||||
|
|
||||||
// generateSnapshot regenerates a brand new snapshot based on an existing state
|
|
||||||
// database and head block asynchronously. The snapshot is returned immediately
|
|
||||||
// and generation is continued in the background until done.
|
|
||||||
func generateSnapshot(diskdb ethdb.KeyValueStore, triedb *trie.Database, cache int, root common.Hash) *diskLayer {
|
|
||||||
// Create a new disk layer with an initialized state marker at zero
|
|
||||||
var (
|
|
||||||
stats = &generatorStats{start: time.Now()}
|
|
||||||
batch = diskdb.NewBatch()
|
|
||||||
genMarker = []byte{} // Initialized but empty!
|
|
||||||
)
|
|
||||||
rawdb.WriteSnapshotRoot(batch, root)
|
|
||||||
journalProgress(batch, genMarker, stats)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed to write initialized state marker", "err", err)
|
|
||||||
}
|
|
||||||
base := &diskLayer{
|
|
||||||
diskdb: diskdb,
|
|
||||||
triedb: triedb,
|
|
||||||
root: root,
|
|
||||||
cache: fastcache.New(cache * 1024 * 1024),
|
|
||||||
genMarker: genMarker,
|
|
||||||
genPending: make(chan struct{}),
|
|
||||||
genAbort: make(chan chan *generatorStats),
|
|
||||||
}
|
|
||||||
go base.generate(stats)
|
|
||||||
log.Debug("Start snapshot generation", "root", root)
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
// journalProgress persists the generator stats into the database to resume later.
|
|
||||||
func journalProgress(db ethdb.KeyValueWriter, marker []byte, stats *generatorStats) {
|
|
||||||
// Write out the generator marker. Note it's a standalone disk layer generator
|
|
||||||
// which is not mixed with journal. It's ok if the generator is persisted while
|
|
||||||
// journal is not.
|
|
||||||
entry := journalGenerator{
|
|
||||||
Done: marker == nil,
|
|
||||||
Marker: marker,
|
|
||||||
}
|
|
||||||
if stats != nil {
|
|
||||||
entry.Accounts = stats.accounts
|
|
||||||
entry.Slots = stats.slots
|
|
||||||
entry.Storage = uint64(stats.storage)
|
|
||||||
}
|
|
||||||
blob, err := rlp.EncodeToBytes(entry)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // Cannot happen, here to catch dev errors
|
|
||||||
}
|
|
||||||
var logstr string
|
|
||||||
switch {
|
|
||||||
case marker == nil:
|
|
||||||
logstr = "done"
|
|
||||||
case bytes.Equal(marker, []byte{}):
|
|
||||||
logstr = "empty"
|
|
||||||
case len(marker) == common.HashLength:
|
|
||||||
logstr = fmt.Sprintf("%#x", marker)
|
|
||||||
default:
|
|
||||||
logstr = fmt.Sprintf("%#x:%#x", marker[:common.HashLength], marker[common.HashLength:])
|
|
||||||
}
|
|
||||||
log.Debug("Journalled generator progress", "progress", logstr)
|
|
||||||
rawdb.WriteSnapshotGenerator(db, blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
// proofResult contains the output of range proving which can be used
|
|
||||||
// for further processing regardless if it is successful or not.
|
|
||||||
type proofResult struct {
|
|
||||||
keys [][]byte // The key set of all elements being iterated, even proving is failed
|
|
||||||
vals [][]byte // The val set of all elements being iterated, even proving is failed
|
|
||||||
diskMore bool // Set when the database has extra snapshot states since last iteration
|
|
||||||
trieMore bool // Set when the trie has extra snapshot states(only meaningful for successful proving)
|
|
||||||
proofErr error // Indicator whether the given state range is valid or not
|
|
||||||
tr *trie.Trie // The trie, in case the trie was resolved by the prover (may be nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// valid returns the indicator that range proof is successful or not.
|
|
||||||
func (result *proofResult) valid() bool {
|
|
||||||
return result.proofErr == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// last returns the last verified element key regardless of whether the range proof is
|
|
||||||
// successful or not. Nil is returned if nothing involved in the proving.
|
|
||||||
func (result *proofResult) last() []byte {
|
|
||||||
var last []byte
|
|
||||||
if len(result.keys) > 0 {
|
|
||||||
last = result.keys[len(result.keys)-1]
|
|
||||||
}
|
|
||||||
return last
|
|
||||||
}
|
|
||||||
|
|
||||||
// forEach iterates all the visited elements and applies the given callback on them.
|
|
||||||
// The iteration is aborted if the callback returns non-nil error.
|
|
||||||
func (result *proofResult) forEach(callback func(key []byte, val []byte) error) error {
|
|
||||||
for i := 0; i < len(result.keys); i++ {
|
|
||||||
key, val := result.keys[i], result.vals[i]
|
|
||||||
if err := callback(key, val); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// proveRange proves the snapshot segment with particular prefix is "valid".
|
|
||||||
// The iteration start point will be assigned if the iterator is restored from
|
|
||||||
// the last interruption. Max will be assigned in order to limit the maximum
|
|
||||||
// amount of data involved in each iteration.
|
|
||||||
//
|
|
||||||
// The proof result will be returned if the range proving is finished, otherwise
|
|
||||||
// the error will be returned to abort the entire procedure.
|
|
||||||
func (dl *diskLayer) proveRange(ctx *generatorContext, trieId *trie.ID, prefix []byte, kind string, origin []byte, max int, valueConvertFn func([]byte) ([]byte, error)) (*proofResult, error) {
|
|
||||||
var (
|
|
||||||
keys [][]byte
|
|
||||||
vals [][]byte
|
|
||||||
proof = rawdb.NewMemoryDatabase()
|
|
||||||
diskMore = false
|
|
||||||
iter = ctx.iterator(kind)
|
|
||||||
start = time.Now()
|
|
||||||
min = append(prefix, origin...)
|
|
||||||
)
|
|
||||||
for iter.Next() {
|
|
||||||
// Ensure the iterated item is always equal or larger than the given origin.
|
|
||||||
key := iter.Key()
|
|
||||||
if bytes.Compare(key, min) < 0 {
|
|
||||||
return nil, errors.New("invalid iteration position")
|
|
||||||
}
|
|
||||||
// Ensure the iterated item still fall in the specified prefix. If
|
|
||||||
// not which means the items in the specified area are all visited.
|
|
||||||
// Move the iterator a step back since we iterate one extra element
|
|
||||||
// out.
|
|
||||||
if !bytes.Equal(key[:len(prefix)], prefix) {
|
|
||||||
iter.Hold()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Break if we've reached the max size, and signal that we're not
|
|
||||||
// done yet. Move the iterator a step back since we iterate one
|
|
||||||
// extra element out.
|
|
||||||
if len(keys) == max {
|
|
||||||
iter.Hold()
|
|
||||||
diskMore = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
keys = append(keys, common.CopyBytes(key[len(prefix):]))
|
|
||||||
|
|
||||||
if valueConvertFn == nil {
|
|
||||||
vals = append(vals, common.CopyBytes(iter.Value()))
|
|
||||||
} else {
|
|
||||||
val, err := valueConvertFn(iter.Value())
|
|
||||||
if err != nil {
|
|
||||||
// Special case, the state data is corrupted (invalid slim-format account),
|
|
||||||
// don't abort the entire procedure directly. Instead, let the fallback
|
|
||||||
// generation to heal the invalid data.
|
|
||||||
//
|
|
||||||
// Here append the original value to ensure that the number of key and
|
|
||||||
// value are aligned.
|
|
||||||
vals = append(vals, common.CopyBytes(iter.Value()))
|
|
||||||
log.Error("Failed to convert account state data", "err", err)
|
|
||||||
} else {
|
|
||||||
vals = append(vals, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update metrics for database iteration and merkle proving
|
|
||||||
if kind == snapStorage {
|
|
||||||
snapStorageSnapReadCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
} else {
|
|
||||||
snapAccountSnapReadCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
}
|
|
||||||
defer func(start time.Time) {
|
|
||||||
if kind == snapStorage {
|
|
||||||
snapStorageProveCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
} else {
|
|
||||||
snapAccountProveCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
}
|
|
||||||
}(time.Now())
|
|
||||||
|
|
||||||
// The snap state is exhausted, pass the entire key/val set for verification
|
|
||||||
root := trieId.Root
|
|
||||||
if origin == nil && !diskMore {
|
|
||||||
stackTr := trie.NewStackTrie(nil)
|
|
||||||
for i, key := range keys {
|
|
||||||
if err := stackTr.Update(key, vals[i]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if gotRoot := stackTr.Hash(); gotRoot != root {
|
|
||||||
return &proofResult{
|
|
||||||
keys: keys,
|
|
||||||
vals: vals,
|
|
||||||
proofErr: fmt.Errorf("wrong root: have %#x want %#x", gotRoot, root),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return &proofResult{keys: keys, vals: vals}, nil
|
|
||||||
}
|
|
||||||
// Snap state is chunked, generate edge proofs for verification.
|
|
||||||
tr, err := trie.New(trieId, dl.triedb)
|
|
||||||
if err != nil {
|
|
||||||
ctx.stats.Log("Trie missing, state snapshotting paused", dl.root, dl.genMarker)
|
|
||||||
return nil, errMissingTrie
|
|
||||||
}
|
|
||||||
// Generate the Merkle proofs for the first and last element
|
|
||||||
if origin == nil {
|
|
||||||
origin = common.Hash{}.Bytes()
|
|
||||||
}
|
|
||||||
if err := tr.Prove(origin, proof); err != nil {
|
|
||||||
log.Debug("Failed to prove range", "kind", kind, "origin", origin, "err", err)
|
|
||||||
return &proofResult{
|
|
||||||
keys: keys,
|
|
||||||
vals: vals,
|
|
||||||
diskMore: diskMore,
|
|
||||||
proofErr: err,
|
|
||||||
tr: tr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if len(keys) > 0 {
|
|
||||||
if err := tr.Prove(keys[len(keys)-1], proof); err != nil {
|
|
||||||
log.Debug("Failed to prove range", "kind", kind, "last", keys[len(keys)-1], "err", err)
|
|
||||||
return &proofResult{
|
|
||||||
keys: keys,
|
|
||||||
vals: vals,
|
|
||||||
diskMore: diskMore,
|
|
||||||
proofErr: err,
|
|
||||||
tr: tr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Verify the snapshot segment with range prover, ensure that all flat states
|
|
||||||
// in this range correspond to merkle trie.
|
|
||||||
cont, err := trie.VerifyRangeProof(root, origin, keys, vals, proof)
|
|
||||||
return &proofResult{
|
|
||||||
keys: keys,
|
|
||||||
vals: vals,
|
|
||||||
diskMore: diskMore,
|
|
||||||
trieMore: cont,
|
|
||||||
proofErr: err,
|
|
||||||
tr: tr},
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// onStateCallback is a function that is called by generateRange, when processing a range of
|
|
||||||
// accounts or storage slots. For each element, the callback is invoked.
|
|
||||||
//
|
|
||||||
// - If 'delete' is true, then this element (and potential slots) needs to be deleted from the snapshot.
|
|
||||||
// - If 'write' is true, then this element needs to be updated with the 'val'.
|
|
||||||
// - If 'write' is false, then this element is already correct, and needs no update.
|
|
||||||
// The 'val' is the canonical encoding of the value (not the slim format for accounts)
|
|
||||||
//
|
|
||||||
// However, for accounts, the storage trie of the account needs to be checked. Also,
|
|
||||||
// dangling storages(storage exists but the corresponding account is missing) need to
|
|
||||||
// be cleaned up.
|
|
||||||
type onStateCallback func(key []byte, val []byte, write bool, delete bool) error
|
|
||||||
|
|
||||||
// generateRange generates the state segment with particular prefix. Generation can
|
|
||||||
// either verify the correctness of existing state through range-proof and skip
|
|
||||||
// generation, or iterate trie to regenerate state on demand.
|
|
||||||
func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefix []byte, kind string, origin []byte, max int, onState onStateCallback, valueConvertFn func([]byte) ([]byte, error)) (bool, []byte, error) {
|
|
||||||
// Use range prover to check the validity of the flat state in the range
|
|
||||||
result, err := dl.proveRange(ctx, trieId, prefix, kind, origin, max, valueConvertFn)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
last := result.last()
|
|
||||||
|
|
||||||
// Construct contextual logger
|
|
||||||
logCtx := []interface{}{"kind", kind, "prefix", hexutil.Encode(prefix)}
|
|
||||||
if len(origin) > 0 {
|
|
||||||
logCtx = append(logCtx, "origin", hexutil.Encode(origin))
|
|
||||||
}
|
|
||||||
logger := log.New(logCtx...)
|
|
||||||
|
|
||||||
// The range prover says the range is correct, skip trie iteration
|
|
||||||
if result.valid() {
|
|
||||||
snapSuccessfulRangeProofMeter.Mark(1)
|
|
||||||
logger.Trace("Proved state range", "last", hexutil.Encode(last))
|
|
||||||
|
|
||||||
// The verification is passed, process each state with the given
|
|
||||||
// callback function. If this state represents a contract, the
|
|
||||||
// corresponding storage check will be performed in the callback
|
|
||||||
if err := result.forEach(func(key []byte, val []byte) error { return onState(key, val, false, false) }); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
// Only abort the iteration when both database and trie are exhausted
|
|
||||||
return !result.diskMore && !result.trieMore, last, nil
|
|
||||||
}
|
|
||||||
logger.Trace("Detected outdated state range", "last", hexutil.Encode(last), "err", result.proofErr)
|
|
||||||
snapFailedRangeProofMeter.Mark(1)
|
|
||||||
|
|
||||||
// Special case, the entire trie is missing. In the original trie scheme,
|
|
||||||
// all the duplicated subtries will be filtered out (only one copy of data
|
|
||||||
// will be stored). While in the snapshot model, all the storage tries
|
|
||||||
// belong to different contracts will be kept even they are duplicated.
|
|
||||||
// Track it to a certain extent remove the noise data used for statistics.
|
|
||||||
if origin == nil && last == nil {
|
|
||||||
meter := snapMissallAccountMeter
|
|
||||||
if kind == snapStorage {
|
|
||||||
meter = snapMissallStorageMeter
|
|
||||||
}
|
|
||||||
meter.Mark(1)
|
|
||||||
}
|
|
||||||
// We use the snap data to build up a cache which can be used by the
|
|
||||||
// main account trie as a primary lookup when resolving hashes
|
|
||||||
var resolver trie.NodeResolver
|
|
||||||
if len(result.keys) > 0 {
|
|
||||||
mdb := rawdb.NewMemoryDatabase()
|
|
||||||
tdb := trie.NewDatabase(mdb, trie.HashDefaults)
|
|
||||||
defer tdb.Close()
|
|
||||||
snapTrie := trie.NewEmpty(tdb)
|
|
||||||
for i, key := range result.keys {
|
|
||||||
snapTrie.Update(key, result.vals[i])
|
|
||||||
}
|
|
||||||
root, nodes, err := snapTrie.Commit(false)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
if nodes != nil {
|
|
||||||
tdb.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), nil)
|
|
||||||
tdb.Commit(root, false)
|
|
||||||
}
|
|
||||||
resolver = func(owner common.Hash, path []byte, hash common.Hash) []byte {
|
|
||||||
return rawdb.ReadTrieNode(mdb, owner, path, hash, tdb.Scheme())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Construct the trie for state iteration, reuse the trie
|
|
||||||
// if it's already opened with some nodes resolved.
|
|
||||||
tr := result.tr
|
|
||||||
if tr == nil {
|
|
||||||
tr, err = trie.New(trieId, dl.triedb)
|
|
||||||
if err != nil {
|
|
||||||
ctx.stats.Log("Trie missing, state snapshotting paused", dl.root, dl.genMarker)
|
|
||||||
return false, nil, errMissingTrie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
trieMore bool
|
|
||||||
kvkeys, kvvals = result.keys, result.vals
|
|
||||||
|
|
||||||
// counters
|
|
||||||
count = 0 // number of states delivered by iterator
|
|
||||||
created = 0 // states created from the trie
|
|
||||||
updated = 0 // states updated from the trie
|
|
||||||
deleted = 0 // states not in trie, but were in snapshot
|
|
||||||
untouched = 0 // states already correct
|
|
||||||
|
|
||||||
// timers
|
|
||||||
start = time.Now()
|
|
||||||
internal time.Duration
|
|
||||||
)
|
|
||||||
nodeIt, err := tr.NodeIterator(origin)
|
|
||||||
if err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
nodeIt.AddResolver(resolver)
|
|
||||||
iter := trie.NewIterator(nodeIt)
|
|
||||||
|
|
||||||
for iter.Next() {
|
|
||||||
if last != nil && bytes.Compare(iter.Key, last) > 0 {
|
|
||||||
trieMore = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
write := true
|
|
||||||
created++
|
|
||||||
for len(kvkeys) > 0 {
|
|
||||||
if cmp := bytes.Compare(kvkeys[0], iter.Key); cmp < 0 {
|
|
||||||
// delete the key
|
|
||||||
istart := time.Now()
|
|
||||||
if err := onState(kvkeys[0], nil, false, true); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
kvkeys = kvkeys[1:]
|
|
||||||
kvvals = kvvals[1:]
|
|
||||||
deleted++
|
|
||||||
internal += time.Since(istart)
|
|
||||||
continue
|
|
||||||
} else if cmp == 0 {
|
|
||||||
// the snapshot key can be overwritten
|
|
||||||
created--
|
|
||||||
if write = !bytes.Equal(kvvals[0], iter.Value); write {
|
|
||||||
updated++
|
|
||||||
} else {
|
|
||||||
untouched++
|
|
||||||
}
|
|
||||||
kvkeys = kvkeys[1:]
|
|
||||||
kvvals = kvvals[1:]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
istart := time.Now()
|
|
||||||
if err := onState(iter.Key, iter.Value, write, false); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
internal += time.Since(istart)
|
|
||||||
}
|
|
||||||
if iter.Err != nil {
|
|
||||||
// Trie errors should never happen. Still, in case of a bug, expose the
|
|
||||||
// error here, as the outer code will presume errors are interrupts, not
|
|
||||||
// some deeper issues.
|
|
||||||
log.Error("State snapshotter failed to iterate trie", "err", iter.Err)
|
|
||||||
return false, nil, iter.Err
|
|
||||||
}
|
|
||||||
// Delete all stale snapshot states remaining
|
|
||||||
istart := time.Now()
|
|
||||||
for _, key := range kvkeys {
|
|
||||||
if err := onState(key, nil, false, true); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
deleted += 1
|
|
||||||
}
|
|
||||||
internal += time.Since(istart)
|
|
||||||
|
|
||||||
// Update metrics for counting trie iteration
|
|
||||||
if kind == snapStorage {
|
|
||||||
snapStorageTrieReadCounter.Inc((time.Since(start) - internal).Nanoseconds())
|
|
||||||
} else {
|
|
||||||
snapAccountTrieReadCounter.Inc((time.Since(start) - internal).Nanoseconds())
|
|
||||||
}
|
|
||||||
logger.Debug("Regenerated state range", "root", trieId.Root, "last", hexutil.Encode(last),
|
|
||||||
"count", count, "created", created, "updated", updated, "untouched", untouched, "deleted", deleted)
|
|
||||||
|
|
||||||
// If there are either more trie items, or there are more snap items
|
|
||||||
// (in the next segment), then we need to keep working
|
|
||||||
return !trieMore && !result.diskMore, last, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndFlush checks if an interruption signal is received or the
|
|
||||||
// batch size has exceeded the allowance.
|
|
||||||
func (dl *diskLayer) checkAndFlush(ctx *generatorContext, current []byte) error {
|
|
||||||
var abort chan *generatorStats
|
|
||||||
select {
|
|
||||||
case abort = <-dl.genAbort:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if ctx.batch.ValueSize() > ethdb.IdealBatchSize || abort != nil {
|
|
||||||
if bytes.Compare(current, dl.genMarker) < 0 {
|
|
||||||
log.Error("Snapshot generator went backwards", "current", fmt.Sprintf("%x", current), "genMarker", fmt.Sprintf("%x", dl.genMarker))
|
|
||||||
}
|
|
||||||
// Flush out the batch anyway no matter it's empty or not.
|
|
||||||
// It's possible that all the states are recovered and the
|
|
||||||
// generation indeed makes progress.
|
|
||||||
journalProgress(ctx.batch, current, ctx.stats)
|
|
||||||
|
|
||||||
if err := ctx.batch.Write(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ctx.batch.Reset()
|
|
||||||
|
|
||||||
dl.lock.Lock()
|
|
||||||
dl.genMarker = current
|
|
||||||
dl.lock.Unlock()
|
|
||||||
|
|
||||||
if abort != nil {
|
|
||||||
ctx.stats.Log("Aborting state snapshot generation", dl.root, current)
|
|
||||||
return newAbortErr(abort) // bubble up an error for interruption
|
|
||||||
}
|
|
||||||
// Don't hold the iterators too long, release them to let compactor works
|
|
||||||
ctx.reopenIterator(snapAccount)
|
|
||||||
ctx.reopenIterator(snapStorage)
|
|
||||||
}
|
|
||||||
if time.Since(ctx.logged) > 8*time.Second {
|
|
||||||
ctx.stats.Log("Generating state snapshot", dl.root, current)
|
|
||||||
ctx.logged = time.Now()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateStorages generates the missing storage slots of the specific contract.
|
|
||||||
// It's supposed to restart the generation from the given origin position.
|
|
||||||
func generateStorages(ctx *generatorContext, dl *diskLayer, stateRoot common.Hash, account common.Hash, storageRoot common.Hash, storeMarker []byte) error {
|
|
||||||
onStorage := func(key []byte, val []byte, write bool, delete bool) error {
|
|
||||||
defer func(start time.Time) {
|
|
||||||
snapStorageWriteCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
}(time.Now())
|
|
||||||
|
|
||||||
if delete {
|
|
||||||
rawdb.DeleteStorageSnapshot(ctx.batch, account, common.BytesToHash(key))
|
|
||||||
snapWipedStorageMeter.Mark(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if write {
|
|
||||||
rawdb.WriteStorageSnapshot(ctx.batch, account, common.BytesToHash(key), val)
|
|
||||||
snapGeneratedStorageMeter.Mark(1)
|
|
||||||
} else {
|
|
||||||
snapRecoveredStorageMeter.Mark(1)
|
|
||||||
}
|
|
||||||
ctx.stats.storage += common.StorageSize(1 + 2*common.HashLength + len(val))
|
|
||||||
ctx.stats.slots++
|
|
||||||
|
|
||||||
// If we've exceeded our batch allowance or termination was requested, flush to disk
|
|
||||||
if err := dl.checkAndFlush(ctx, append(account[:], key...)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Loop for re-generating the missing storage slots.
|
|
||||||
var origin = common.CopyBytes(storeMarker)
|
|
||||||
for {
|
|
||||||
id := trie.StorageTrieID(stateRoot, account, storageRoot)
|
|
||||||
exhausted, last, err := dl.generateRange(ctx, id, append(rawdb.SnapshotStoragePrefix, account.Bytes()...), snapStorage, origin, storageCheckRange, onStorage, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err // The procedure it aborted, either by external signal or internal error.
|
|
||||||
}
|
|
||||||
// Abort the procedure if the entire contract storage is generated
|
|
||||||
if exhausted {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if origin = increaseKey(last); origin == nil {
|
|
||||||
break // special case, the last is 0xffffffff...fff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateAccounts generates the missing snapshot accounts as well as their
|
|
||||||
// storage slots in the main trie. It's supposed to restart the generation
|
|
||||||
// from the given origin position.
|
|
||||||
func generateAccounts(ctx *generatorContext, dl *diskLayer, accMarker []byte) error {
|
|
||||||
onAccount := func(key []byte, val []byte, write bool, delete bool) error {
|
|
||||||
// Make sure to clear all dangling storages before this account
|
|
||||||
account := common.BytesToHash(key)
|
|
||||||
ctx.removeStorageBefore(account)
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
if delete {
|
|
||||||
rawdb.DeleteAccountSnapshot(ctx.batch, account)
|
|
||||||
snapWipedAccountMeter.Mark(1)
|
|
||||||
snapAccountWriteCounter.Inc(time.Since(start).Nanoseconds())
|
|
||||||
|
|
||||||
ctx.removeStorageAt(account)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Retrieve the current account and flatten it into the internal format
|
|
||||||
var acc types.StateAccount
|
|
||||||
if err := rlp.DecodeBytes(val, &acc); err != nil {
|
|
||||||
log.Crit("Invalid account encountered during snapshot creation", "err", err)
|
|
||||||
}
|
|
||||||
// If the account is not yet in-progress, write it out
|
|
||||||
if accMarker == nil || !bytes.Equal(account[:], accMarker) {
|
|
||||||
dataLen := len(val) // Approximate size, saves us a round of RLP-encoding
|
|
||||||
if !write {
|
|
||||||
if bytes.Equal(acc.CodeHash, types.EmptyCodeHash[:]) {
|
|
||||||
dataLen -= 32
|
|
||||||
}
|
|
||||||
if acc.Root == types.EmptyRootHash {
|
|
||||||
dataLen -= 32
|
|
||||||
}
|
|
||||||
snapRecoveredAccountMeter.Mark(1)
|
|
||||||
} else {
|
|
||||||
data := types.SlimAccountRLP(acc)
|
|
||||||
dataLen = len(data)
|
|
||||||
rawdb.WriteAccountSnapshot(ctx.batch, account, data)
|
|
||||||
snapGeneratedAccountMeter.Mark(1)
|
|
||||||
}
|
|
||||||
ctx.stats.storage += common.StorageSize(1 + common.HashLength + dataLen)
|
|
||||||
ctx.stats.accounts++
|
|
||||||
}
|
|
||||||
// If the snap generation goes here after interrupted, genMarker may go backward
|
|
||||||
// when last genMarker is consisted of accountHash and storageHash
|
|
||||||
marker := account[:]
|
|
||||||
if accMarker != nil && bytes.Equal(marker, accMarker) && len(dl.genMarker) > common.HashLength {
|
|
||||||
marker = dl.genMarker[:]
|
|
||||||
}
|
|
||||||
// If we've exceeded our batch allowance or termination was requested, flush to disk
|
|
||||||
if err := dl.checkAndFlush(ctx, marker); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
snapAccountWriteCounter.Inc(time.Since(start).Nanoseconds()) // let's count flush time as well
|
|
||||||
|
|
||||||
// If the iterated account is the contract, create a further loop to
|
|
||||||
// verify or regenerate the contract storage.
|
|
||||||
if acc.Root == types.EmptyRootHash {
|
|
||||||
ctx.removeStorageAt(account)
|
|
||||||
} else {
|
|
||||||
var storeMarker []byte
|
|
||||||
if accMarker != nil && bytes.Equal(account[:], accMarker) && len(dl.genMarker) > common.HashLength {
|
|
||||||
storeMarker = dl.genMarker[common.HashLength:]
|
|
||||||
}
|
|
||||||
if err := generateStorages(ctx, dl, dl.root, account, acc.Root, storeMarker); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Some account processed, unmark the marker
|
|
||||||
accMarker = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Always reset the initial account range as 1 whenever recover from the
|
|
||||||
// interruption. TODO(rjl493456442) can we remove it?
|
|
||||||
var accountRange = accountCheckRange
|
|
||||||
if len(accMarker) > 0 {
|
|
||||||
accountRange = 1
|
|
||||||
}
|
|
||||||
origin := common.CopyBytes(accMarker)
|
|
||||||
for {
|
|
||||||
id := trie.StateTrieID(dl.root)
|
|
||||||
exhausted, last, err := dl.generateRange(ctx, id, rawdb.SnapshotAccountPrefix, snapAccount, origin, accountRange, onAccount, types.FullAccountRLP)
|
|
||||||
if err != nil {
|
|
||||||
return err // The procedure it aborted, either by external signal or internal error.
|
|
||||||
}
|
|
||||||
origin = increaseKey(last)
|
|
||||||
|
|
||||||
// Last step, cleanup the storages after the last account.
|
|
||||||
// All the left storages should be treated as dangling.
|
|
||||||
if origin == nil || exhausted {
|
|
||||||
ctx.removeStorageLeft()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
accountRange = accountCheckRange
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate is a background thread that iterates over the state and storage tries,
|
|
||||||
// constructing the state snapshot. All the arguments are purely for statistics
|
|
||||||
// gathering and logging, since the method surfs the blocks as they arrive, often
|
|
||||||
// being restarted.
|
|
||||||
func (dl *diskLayer) generate(stats *generatorStats) {
|
|
||||||
var (
|
|
||||||
accMarker []byte
|
|
||||||
abort chan *generatorStats
|
|
||||||
)
|
|
||||||
if len(dl.genMarker) > 0 { // []byte{} is the start, use nil for that
|
|
||||||
accMarker = dl.genMarker[:common.HashLength]
|
|
||||||
}
|
|
||||||
stats.Log("Resuming state snapshot generation", dl.root, dl.genMarker)
|
|
||||||
|
|
||||||
// Initialize the global generator context. The snapshot iterators are
|
|
||||||
// opened at the interrupted position because the assumption is held
|
|
||||||
// that all the snapshot data are generated correctly before the marker.
|
|
||||||
// Even if the snapshot data is updated during the interruption (before
|
|
||||||
// or at the marker), the assumption is still held.
|
|
||||||
// For the account or storage slot at the interruption, they will be
|
|
||||||
// processed twice by the generator(they are already processed in the
|
|
||||||
// last run) but it's fine.
|
|
||||||
ctx := newGeneratorContext(stats, dl.diskdb, accMarker, dl.genMarker)
|
|
||||||
defer ctx.close()
|
|
||||||
|
|
||||||
if err := generateAccounts(ctx, dl, accMarker); err != nil {
|
|
||||||
// Extract the received interruption signal if exists
|
|
||||||
if aerr, ok := err.(*abortErr); ok {
|
|
||||||
abort = aerr.abort
|
|
||||||
}
|
|
||||||
// Aborted by internal error, wait the signal
|
|
||||||
if abort == nil {
|
|
||||||
abort = <-dl.genAbort
|
|
||||||
}
|
|
||||||
abort <- stats
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Snapshot fully generated, set the marker to nil.
|
|
||||||
// Note even there is nothing to commit, persist the
|
|
||||||
// generator anyway to mark the snapshot is complete.
|
|
||||||
journalProgress(ctx.batch, nil, stats)
|
|
||||||
if err := ctx.batch.Write(); err != nil {
|
|
||||||
log.Error("Failed to flush batch", "err", err)
|
|
||||||
|
|
||||||
abort = <-dl.genAbort
|
|
||||||
abort <- stats
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.batch.Reset()
|
|
||||||
|
|
||||||
log.Info("Generated state snapshot", "accounts", stats.accounts, "slots", stats.slots,
|
|
||||||
"storage", stats.storage, "dangling", stats.dangling, "elapsed", common.PrettyDuration(time.Since(stats.start)))
|
|
||||||
|
|
||||||
dl.lock.Lock()
|
|
||||||
dl.genMarker = nil
|
|
||||||
close(dl.genPending)
|
|
||||||
dl.lock.Unlock()
|
|
||||||
|
|
||||||
// Someone will be looking for us, wait it out
|
|
||||||
abort = <-dl.genAbort
|
|
||||||
abort <- nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// increaseKey increase the input key by one bit. Return nil if the entire
|
|
||||||
// addition operation overflows.
|
|
||||||
func increaseKey(key []byte) []byte {
|
|
||||||
for i := len(key) - 1; i >= 0; i-- {
|
|
||||||
key[i]++
|
|
||||||
if key[i] != 0x0 {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// abortErr wraps an interruption signal received to represent the
|
|
||||||
// generation is aborted by external processes.
|
|
||||||
type abortErr struct {
|
|
||||||
abort chan *generatorStats
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAbortErr(abort chan *generatorStats) error {
|
|
||||||
return &abortErr{abort: abort}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *abortErr) Error() string {
|
|
||||||
return "aborted"
|
|
||||||
}
|
|
||||||
|
|
@ -1,968 +0,0 @@
|
||||||
// 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/triedb/hashdb"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/triedb/pathdb"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func hashData(input []byte) common.Hash {
|
|
||||||
var hasher = sha3.NewLegacyKeccak256()
|
|
||||||
var hash common.Hash
|
|
||||||
hasher.Reset()
|
|
||||||
hasher.Write(input)
|
|
||||||
hasher.Sum(hash[:0])
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation from an empty database.
|
|
||||||
func TestGeneration(t *testing.T) {
|
|
||||||
testGeneration(t, rawdb.HashScheme)
|
|
||||||
testGeneration(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGeneration(t *testing.T, scheme string) {
|
|
||||||
// We can't use statedb to make a test trie (circular dependency), so make
|
|
||||||
// a fake one manually. We're going with a small account trie of 3 accounts,
|
|
||||||
// two of which also has the same 3-slot storage trie attached.
|
|
||||||
var helper = newHelper(scheme)
|
|
||||||
stRoot := helper.makeStorageTrie(common.Hash{}, []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, false)
|
|
||||||
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
if have, want := root, common.HexToHash("0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd"); have != want {
|
|
||||||
t.Fatalf("have %#x want %#x", have, want)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation with existent flat state.
|
|
||||||
func TestGenerateExistentState(t *testing.T) {
|
|
||||||
testGenerateExistentState(t, rawdb.HashScheme)
|
|
||||||
testGenerateExistentState(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateExistentState(t *testing.T, scheme string) {
|
|
||||||
// We can't use statedb to make a test trie (circular dependency), so make
|
|
||||||
// a fake one manually. We're going with a small account trie of 3 accounts,
|
|
||||||
// two of which also has the same 3-slot storage trie attached.
|
|
||||||
var helper = newHelper(scheme)
|
|
||||||
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-1", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
stRoot = helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-3", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSnapRoot(t *testing.T, snap *diskLayer, trieRoot common.Hash) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
accIt := snap.AccountIterator(common.Hash{})
|
|
||||||
defer accIt.Release()
|
|
||||||
|
|
||||||
snapRoot, err := generateTrieRoot(nil, "", accIt, common.Hash{}, stackTrieGenerate,
|
|
||||||
func(db ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error) {
|
|
||||||
storageIt, _ := snap.StorageIterator(accountHash, common.Hash{})
|
|
||||||
defer storageIt.Release()
|
|
||||||
|
|
||||||
hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false)
|
|
||||||
if err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
return hash, nil
|
|
||||||
}, newGenerateStats(), true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if snapRoot != trieRoot {
|
|
||||||
t.Fatalf("snaproot: %#x != trieroot #%x", snapRoot, trieRoot)
|
|
||||||
}
|
|
||||||
if err := CheckDanglingStorage(snap.diskdb); err != nil {
|
|
||||||
t.Fatalf("Detected dangling storages: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type testHelper struct {
|
|
||||||
diskdb ethdb.Database
|
|
||||||
triedb *trie.Database
|
|
||||||
accTrie *trie.StateTrie
|
|
||||||
nodes *trienode.MergedNodeSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHelper(scheme string) *testHelper {
|
|
||||||
diskdb := rawdb.NewMemoryDatabase()
|
|
||||||
config := &trie.Config{}
|
|
||||||
if scheme == rawdb.PathScheme {
|
|
||||||
config.PathDB = &pathdb.Config{} // disable caching
|
|
||||||
} else {
|
|
||||||
config.HashDB = &hashdb.Config{} // disable caching
|
|
||||||
}
|
|
||||||
triedb := trie.NewDatabase(diskdb, config)
|
|
||||||
accTrie, _ := trie.NewStateTrie(trie.StateTrieID(types.EmptyRootHash), triedb)
|
|
||||||
return &testHelper{
|
|
||||||
diskdb: diskdb,
|
|
||||||
triedb: triedb,
|
|
||||||
accTrie: accTrie,
|
|
||||||
nodes: trienode.NewMergedNodeSet(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) addTrieAccount(acckey string, acc *types.StateAccount) {
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
t.accTrie.MustUpdate([]byte(acckey), val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) addSnapAccount(acckey string, acc *types.StateAccount) {
|
|
||||||
key := hashData([]byte(acckey))
|
|
||||||
rawdb.WriteAccountSnapshot(t.diskdb, key, types.SlimAccountRLP(*acc))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) addAccount(acckey string, acc *types.StateAccount) {
|
|
||||||
t.addTrieAccount(acckey, acc)
|
|
||||||
t.addSnapAccount(acckey, acc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) addSnapStorage(accKey string, keys []string, vals []string) {
|
|
||||||
accHash := hashData([]byte(accKey))
|
|
||||||
for i, key := range keys {
|
|
||||||
rawdb.WriteStorageSnapshot(t.diskdb, accHash, hashData([]byte(key)), []byte(vals[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) makeStorageTrie(owner common.Hash, keys []string, vals []string, commit bool) common.Hash {
|
|
||||||
id := trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash)
|
|
||||||
stTrie, _ := trie.NewStateTrie(id, t.triedb)
|
|
||||||
for i, k := range keys {
|
|
||||||
stTrie.MustUpdate([]byte(k), []byte(vals[i]))
|
|
||||||
}
|
|
||||||
if !commit {
|
|
||||||
return stTrie.Hash()
|
|
||||||
}
|
|
||||||
root, nodes, _ := stTrie.Commit(false)
|
|
||||||
if nodes != nil {
|
|
||||||
t.nodes.Merge(nodes)
|
|
||||||
}
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) Commit() common.Hash {
|
|
||||||
root, nodes, _ := t.accTrie.Commit(true)
|
|
||||||
if nodes != nil {
|
|
||||||
t.nodes.Merge(nodes)
|
|
||||||
}
|
|
||||||
t.triedb.Update(root, types.EmptyRootHash, 0, t.nodes, nil)
|
|
||||||
t.triedb.Commit(root, false)
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testHelper) CommitAndGenerate() (common.Hash, *diskLayer) {
|
|
||||||
root := t.Commit()
|
|
||||||
snap := generateSnapshot(t.diskdb, t.triedb, 16, root)
|
|
||||||
return root, snap
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation with existent flat state, where the flat state
|
|
||||||
// contains some errors:
|
|
||||||
// - the contract with empty storage root but has storage entries in the disk
|
|
||||||
// - the contract with non empty storage root but empty storage slots
|
|
||||||
// - the contract(non-empty storage) misses some storage slots
|
|
||||||
// - miss in the beginning
|
|
||||||
// - miss in the middle
|
|
||||||
// - miss in the end
|
|
||||||
//
|
|
||||||
// - the contract(non-empty storage) has wrong storage slots
|
|
||||||
// - wrong slots in the beginning
|
|
||||||
// - wrong slots in the middle
|
|
||||||
// - wrong slots in the end
|
|
||||||
//
|
|
||||||
// - the contract(non-empty storage) has extra storage slots
|
|
||||||
// - extra slots in the beginning
|
|
||||||
// - extra slots in the middle
|
|
||||||
// - extra slots in the end
|
|
||||||
func TestGenerateExistentStateWithWrongStorage(t *testing.T) {
|
|
||||||
testGenerateExistentStateWithWrongStorage(t, rawdb.HashScheme)
|
|
||||||
testGenerateExistentStateWithWrongStorage(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateExistentStateWithWrongStorage(t *testing.T, scheme string) {
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
|
|
||||||
// Account one, empty root but non-empty database
|
|
||||||
helper.addAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-1", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Account two, non empty root but empty database
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-2")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-2", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
// Miss slots
|
|
||||||
{
|
|
||||||
// Account three, non empty root but misses slots in the beginning
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-3", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-3", []string{"key-2", "key-3"}, []string{"val-2", "val-3"})
|
|
||||||
|
|
||||||
// Account four, non empty root but misses slots in the middle
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-4")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-4", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-4", []string{"key-1", "key-3"}, []string{"val-1", "val-3"})
|
|
||||||
|
|
||||||
// Account five, non empty root but misses slots in the end
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-5")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-5", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-5", []string{"key-1", "key-2"}, []string{"val-1", "val-2"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrong storage slots
|
|
||||||
{
|
|
||||||
// Account six, non empty root but wrong slots in the beginning
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-6")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-6", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-6", []string{"key-1", "key-2", "key-3"}, []string{"badval-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Account seven, non empty root but wrong slots in the middle
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-7")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-7", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-7", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "badval-2", "val-3"})
|
|
||||||
|
|
||||||
// Account eight, non empty root but wrong slots in the end
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-8")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-8", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-8", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "badval-3"})
|
|
||||||
|
|
||||||
// Account 9, non empty root but rotated slots
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-9")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-9", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-9", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-3", "val-2"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra storage slots
|
|
||||||
{
|
|
||||||
// Account 10, non empty root but extra slots in the beginning
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-10")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-10", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-10", []string{"key-0", "key-1", "key-2", "key-3"}, []string{"val-0", "val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Account 11, non empty root but extra slots in the middle
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-11")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-11", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-11", []string{"key-1", "key-2", "key-2-1", "key-3"}, []string{"val-1", "val-2", "val-2-1", "val-3"})
|
|
||||||
|
|
||||||
// Account 12, non empty root but extra slots in the end
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-12")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-12", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapStorage("acc-12", []string{"key-1", "key-2", "key-3", "key-4"}, []string{"val-1", "val-2", "val-3", "val-4"})
|
|
||||||
}
|
|
||||||
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
t.Logf("Root: %#x\n", root) // Root = 0x8746cce9fd9c658b2cfd639878ed6584b7a2b3e73bb40f607fcfa156002429a0
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation with existent flat state, where the flat state
|
|
||||||
// contains some errors:
|
|
||||||
// - miss accounts
|
|
||||||
// - wrong accounts
|
|
||||||
// - extra accounts
|
|
||||||
func TestGenerateExistentStateWithWrongAccounts(t *testing.T) {
|
|
||||||
testGenerateExistentStateWithWrongAccounts(t, rawdb.HashScheme)
|
|
||||||
testGenerateExistentStateWithWrongAccounts(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateExistentStateWithWrongAccounts(t *testing.T, scheme string) {
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-2")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-4")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-6")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
|
|
||||||
// Trie accounts [acc-1, acc-2, acc-3, acc-4, acc-6]
|
|
||||||
// Extra accounts [acc-0, acc-5, acc-7]
|
|
||||||
|
|
||||||
// Missing accounts, only in the trie
|
|
||||||
{
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // Beginning
|
|
||||||
helper.addTrieAccount("acc-4", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // Middle
|
|
||||||
helper.addTrieAccount("acc-6", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // End
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrong accounts
|
|
||||||
{
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapAccount("acc-2", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: common.Hex2Bytes("0x1234")})
|
|
||||||
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addSnapAccount("acc-3", &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra accounts, only in the snap
|
|
||||||
{
|
|
||||||
helper.addSnapAccount("acc-0", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // before the beginning
|
|
||||||
helper.addSnapAccount("acc-5", &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: common.Hex2Bytes("0x1234")}) // Middle
|
|
||||||
helper.addSnapAccount("acc-7", &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) // after the end
|
|
||||||
}
|
|
||||||
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
t.Logf("Root: %#x\n", root) // Root = 0x825891472281463511e7ebcc7f109e4f9200c20fa384754e11fd605cd98464e8
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation errors out correctly in case of a missing trie
|
|
||||||
// node in the account trie.
|
|
||||||
func TestGenerateCorruptAccountTrie(t *testing.T) {
|
|
||||||
testGenerateCorruptAccountTrie(t, rawdb.HashScheme)
|
|
||||||
testGenerateCorruptAccountTrie(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateCorruptAccountTrie(t *testing.T, scheme string) {
|
|
||||||
// We can't use statedb to make a test trie (circular dependency), so make
|
|
||||||
// a fake one manually. We're going with a small account trie of 3 accounts,
|
|
||||||
// without any storage slots to keep the test smaller.
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) // 0xc7a30f39aff471c95d8a837497ad0e49b65be475cc0953540f80cfcdbdcd9074
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x65145f923027566669a1ae5ccac66f945b55ff6eaeb17d2ea8e048b7d381f2d7
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x19ead688e907b0fab07176120dceec244a72aff2f0aa51e8b827584e378772f4
|
|
||||||
|
|
||||||
root := helper.Commit() // Root: 0xa04693ea110a31037fb5ee814308a6f1d76bdab0b11676bdf4541d2de55ba978
|
|
||||||
|
|
||||||
// Delete an account trie node and ensure the generator chokes
|
|
||||||
targetPath := []byte{0xc}
|
|
||||||
targetHash := common.HexToHash("0x65145f923027566669a1ae5ccac66f945b55ff6eaeb17d2ea8e048b7d381f2d7")
|
|
||||||
|
|
||||||
rawdb.DeleteTrieNode(helper.diskdb, common.Hash{}, targetPath, targetHash, scheme)
|
|
||||||
|
|
||||||
snap := generateSnapshot(helper.diskdb, helper.triedb, 16, root)
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
t.Errorf("Snapshot generated against corrupt account trie")
|
|
||||||
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
// Not generated fast enough, hopefully blocked inside on missing trie node fail
|
|
||||||
}
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation errors out correctly in case of a missing root
|
|
||||||
// trie node for a storage trie. It's similar to internal corruption but it is
|
|
||||||
// handled differently inside the generator.
|
|
||||||
func TestGenerateMissingStorageTrie(t *testing.T) {
|
|
||||||
testGenerateMissingStorageTrie(t, rawdb.HashScheme)
|
|
||||||
testGenerateMissingStorageTrie(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateMissingStorageTrie(t *testing.T, scheme string) {
|
|
||||||
// We can't use statedb to make a test trie (circular dependency), so make
|
|
||||||
// a fake one manually. We're going with a small account trie of 3 accounts,
|
|
||||||
// two of which also has the same 3-slot storage trie attached.
|
|
||||||
var (
|
|
||||||
acc1 = hashData([]byte("acc-1"))
|
|
||||||
acc3 = hashData([]byte("acc-3"))
|
|
||||||
helper = newHelper(scheme)
|
|
||||||
)
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true) // 0xddefcd9376dd029653ef384bd2f0a126bb755fe84fdcc9e7cf421ba454f2bc67
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x9250573b9c18c664139f3b6a7a8081b7d8f8916a8fcc5d94feec6c29f5fd4e9e
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x65145f923027566669a1ae5ccac66f945b55ff6eaeb17d2ea8e048b7d381f2d7
|
|
||||||
stRoot = helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x50815097425d000edfc8b3a4a13e175fc2bdcfee8bdfbf2d1ff61041d3c235b2
|
|
||||||
|
|
||||||
root := helper.Commit()
|
|
||||||
|
|
||||||
// Delete storage trie root of account one and three.
|
|
||||||
rawdb.DeleteTrieNode(helper.diskdb, acc1, nil, stRoot, scheme)
|
|
||||||
rawdb.DeleteTrieNode(helper.diskdb, acc3, nil, stRoot, scheme)
|
|
||||||
|
|
||||||
snap := generateSnapshot(helper.diskdb, helper.triedb, 16, root)
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
t.Errorf("Snapshot generated against corrupt storage trie")
|
|
||||||
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
// Not generated fast enough, hopefully blocked inside on missing trie node fail
|
|
||||||
}
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation errors out correctly in case of a missing trie
|
|
||||||
// node in a storage trie.
|
|
||||||
func TestGenerateCorruptStorageTrie(t *testing.T) {
|
|
||||||
testGenerateCorruptStorageTrie(t, rawdb.HashScheme)
|
|
||||||
testGenerateCorruptStorageTrie(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateCorruptStorageTrie(t *testing.T, scheme string) {
|
|
||||||
// We can't use statedb to make a test trie (circular dependency), so make
|
|
||||||
// a fake one manually. We're going with a small account trie of 3 accounts,
|
|
||||||
// two of which also has the same 3-slot storage trie attached.
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true) // 0xddefcd9376dd029653ef384bd2f0a126bb755fe84fdcc9e7cf421ba454f2bc67
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x9250573b9c18c664139f3b6a7a8081b7d8f8916a8fcc5d94feec6c29f5fd4e9e
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x65145f923027566669a1ae5ccac66f945b55ff6eaeb17d2ea8e048b7d381f2d7
|
|
||||||
stRoot = helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}) // 0x50815097425d000edfc8b3a4a13e175fc2bdcfee8bdfbf2d1ff61041d3c235b2
|
|
||||||
|
|
||||||
root := helper.Commit()
|
|
||||||
|
|
||||||
// Delete a node in the storage trie.
|
|
||||||
targetPath := []byte{0x4}
|
|
||||||
targetHash := common.HexToHash("0x18a0f4d79cff4459642dd7604f303886ad9d77c30cf3d7d7cedb3a693ab6d371")
|
|
||||||
rawdb.DeleteTrieNode(helper.diskdb, hashData([]byte("acc-1")), targetPath, targetHash, scheme)
|
|
||||||
rawdb.DeleteTrieNode(helper.diskdb, hashData([]byte("acc-3")), targetPath, targetHash, scheme)
|
|
||||||
|
|
||||||
snap := generateSnapshot(helper.diskdb, helper.triedb, 16, root)
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
t.Errorf("Snapshot generated against corrupt storage trie")
|
|
||||||
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
// Not generated fast enough, hopefully blocked inside on missing trie node fail
|
|
||||||
}
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation when an extra account with storage exists in the snap state.
|
|
||||||
func TestGenerateWithExtraAccounts(t *testing.T) {
|
|
||||||
testGenerateWithExtraAccounts(t, rawdb.HashScheme)
|
|
||||||
testGenerateWithExtraAccounts(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateWithExtraAccounts(t *testing.T, scheme string) {
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
{
|
|
||||||
// Account one in the trie
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")),
|
|
||||||
[]string{"key-1", "key-2", "key-3", "key-4", "key-5"},
|
|
||||||
[]string{"val-1", "val-2", "val-3", "val-4", "val-5"},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
acc := &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
helper.accTrie.MustUpdate([]byte("acc-1"), val) // 0x9250573b9c18c664139f3b6a7a8081b7d8f8916a8fcc5d94feec6c29f5fd4e9e
|
|
||||||
|
|
||||||
// Identical in the snap
|
|
||||||
key := hashData([]byte("acc-1"))
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, key, val)
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-1")), []byte("val-1"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-2")), []byte("val-2"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-3")), []byte("val-3"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-4")), []byte("val-4"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-5")), []byte("val-5"))
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// Account two exists only in the snapshot
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-2")),
|
|
||||||
[]string{"key-1", "key-2", "key-3", "key-4", "key-5"},
|
|
||||||
[]string{"val-1", "val-2", "val-3", "val-4", "val-5"},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
acc := &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
key := hashData([]byte("acc-2"))
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, key, val)
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-1")), []byte("b-val-1"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-2")), []byte("b-val-2"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-3")), []byte("b-val-3"))
|
|
||||||
}
|
|
||||||
root := helper.Commit()
|
|
||||||
|
|
||||||
// To verify the test: If we now inspect the snap db, there should exist extraneous storage items
|
|
||||||
if data := rawdb.ReadStorageSnapshot(helper.diskdb, hashData([]byte("acc-2")), hashData([]byte("b-key-1"))); data == nil {
|
|
||||||
t.Fatalf("expected snap storage to exist")
|
|
||||||
}
|
|
||||||
snap := generateSnapshot(helper.diskdb, helper.triedb, 16, root)
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
// If we now inspect the snap db, there should exist no extraneous storage items
|
|
||||||
if data := rawdb.ReadStorageSnapshot(helper.diskdb, hashData([]byte("acc-2")), hashData([]byte("b-key-1"))); data != nil {
|
|
||||||
t.Fatalf("expected slot to be removed, got %v", string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func enableLogging() {
|
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation when an extra account with storage exists in the snap state.
|
|
||||||
func TestGenerateWithManyExtraAccounts(t *testing.T) {
|
|
||||||
testGenerateWithManyExtraAccounts(t, rawdb.HashScheme)
|
|
||||||
testGenerateWithManyExtraAccounts(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateWithManyExtraAccounts(t *testing.T, scheme string) {
|
|
||||||
if false {
|
|
||||||
enableLogging()
|
|
||||||
}
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
{
|
|
||||||
// Account one in the trie
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")),
|
|
||||||
[]string{"key-1", "key-2", "key-3"},
|
|
||||||
[]string{"val-1", "val-2", "val-3"},
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
acc := &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()}
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
helper.accTrie.MustUpdate([]byte("acc-1"), val) // 0x9250573b9c18c664139f3b6a7a8081b7d8f8916a8fcc5d94feec6c29f5fd4e9e
|
|
||||||
|
|
||||||
// Identical in the snap
|
|
||||||
key := hashData([]byte("acc-1"))
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, key, val)
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-1")), []byte("val-1"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-2")), []byte("val-2"))
|
|
||||||
rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-3")), []byte("val-3"))
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// 100 accounts exist only in snapshot
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
acc := &types.StateAccount{Balance: big.NewInt(int64(i)), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
key := hashData([]byte(fmt.Sprintf("acc-%d", i)))
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, key, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests this case
|
|
||||||
// maxAccountRange 3
|
|
||||||
// snapshot-accounts: 01, 02, 03, 04, 05, 06, 07
|
|
||||||
// trie-accounts: 03, 07
|
|
||||||
//
|
|
||||||
// We iterate three snapshot storage slots (max = 3) from the database. They are 0x01, 0x02, 0x03.
|
|
||||||
// The trie has a lot of deletions.
|
|
||||||
// So in trie, we iterate 2 entries 0x03, 0x07. We create the 0x07 in the database and abort the procedure, because the trie is exhausted.
|
|
||||||
// But in the database, we still have the stale storage slots 0x04, 0x05. They are not iterated yet, but the procedure is finished.
|
|
||||||
func TestGenerateWithExtraBeforeAndAfter(t *testing.T) {
|
|
||||||
testGenerateWithExtraBeforeAndAfter(t, rawdb.HashScheme)
|
|
||||||
testGenerateWithExtraBeforeAndAfter(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateWithExtraBeforeAndAfter(t *testing.T, scheme string) {
|
|
||||||
accountCheckRange = 3
|
|
||||||
if false {
|
|
||||||
enableLogging()
|
|
||||||
}
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
{
|
|
||||||
acc := &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
helper.accTrie.MustUpdate(common.HexToHash("0x03").Bytes(), val)
|
|
||||||
helper.accTrie.MustUpdate(common.HexToHash("0x07").Bytes(), val)
|
|
||||||
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x01"), val)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x02"), val)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x03"), val)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x04"), val)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x05"), val)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x06"), val)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x07"), val)
|
|
||||||
}
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenerateWithMalformedSnapdata tests what happes if we have some junk
|
|
||||||
// in the snapshot database, which cannot be parsed back to an account
|
|
||||||
func TestGenerateWithMalformedSnapdata(t *testing.T) {
|
|
||||||
testGenerateWithMalformedSnapdata(t, rawdb.HashScheme)
|
|
||||||
testGenerateWithMalformedSnapdata(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateWithMalformedSnapdata(t *testing.T, scheme string) {
|
|
||||||
accountCheckRange = 3
|
|
||||||
if false {
|
|
||||||
enableLogging()
|
|
||||||
}
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
{
|
|
||||||
acc := &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}
|
|
||||||
val, _ := rlp.EncodeToBytes(acc)
|
|
||||||
helper.accTrie.MustUpdate(common.HexToHash("0x03").Bytes(), val)
|
|
||||||
|
|
||||||
junk := make([]byte, 100)
|
|
||||||
copy(junk, []byte{0xde, 0xad})
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x02"), junk)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x03"), junk)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x04"), junk)
|
|
||||||
rawdb.WriteAccountSnapshot(helper.diskdb, common.HexToHash("0x05"), junk)
|
|
||||||
}
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
// If we now inspect the snap db, there should exist no extraneous storage items
|
|
||||||
if data := rawdb.ReadStorageSnapshot(helper.diskdb, hashData([]byte("acc-2")), hashData([]byte("b-key-1"))); data != nil {
|
|
||||||
t.Fatalf("expected slot to be removed, got %v", string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateFromEmptySnap(t *testing.T) {
|
|
||||||
testGenerateFromEmptySnap(t, rawdb.HashScheme)
|
|
||||||
testGenerateFromEmptySnap(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateFromEmptySnap(t *testing.T, scheme string) {
|
|
||||||
//enableLogging()
|
|
||||||
accountCheckRange = 10
|
|
||||||
storageCheckRange = 20
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
// Add 1K accounts to the trie
|
|
||||||
for i := 0; i < 400; i++ {
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte(fmt.Sprintf("acc-%d", i))), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount(fmt.Sprintf("acc-%d", i),
|
|
||||||
&types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
}
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
t.Logf("Root: %#x\n", root) // Root: 0x6f7af6d2e1a1bf2b84a3beb3f8b64388465fbc1e274ca5d5d3fc787ca78f59e4
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation with existent flat state, where the flat state
|
|
||||||
// storage is correct, but incomplete.
|
|
||||||
// The incomplete part is on the second range
|
|
||||||
// snap: [ 0x01, 0x02, 0x03, 0x04] , [ 0x05, 0x06, 0x07, {missing}] (with storageCheck = 4)
|
|
||||||
// trie: 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
|
|
||||||
// This hits a case where the snap verification passes, but there are more elements in the trie
|
|
||||||
// which we must also add.
|
|
||||||
func TestGenerateWithIncompleteStorage(t *testing.T) {
|
|
||||||
testGenerateWithIncompleteStorage(t, rawdb.HashScheme)
|
|
||||||
testGenerateWithIncompleteStorage(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateWithIncompleteStorage(t *testing.T, scheme string) {
|
|
||||||
storageCheckRange = 4
|
|
||||||
helper := newHelper(scheme)
|
|
||||||
stKeys := []string{"1", "2", "3", "4", "5", "6", "7", "8"}
|
|
||||||
stVals := []string{"v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"}
|
|
||||||
// We add 8 accounts, each one is missing exactly one of the storage slots. This means
|
|
||||||
// we don't have to order the keys and figure out exactly which hash-key winds up
|
|
||||||
// on the sensitive spots at the boundaries
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
accKey := fmt.Sprintf("acc-%d", i)
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte(accKey)), stKeys, stVals, true)
|
|
||||||
helper.addAccount(accKey, &types.StateAccount{Balance: big.NewInt(int64(i)), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
var moddedKeys []string
|
|
||||||
var moddedVals []string
|
|
||||||
for ii := 0; ii < 8; ii++ {
|
|
||||||
if ii != i {
|
|
||||||
moddedKeys = append(moddedKeys, stKeys[ii])
|
|
||||||
moddedVals = append(moddedVals, stVals[ii])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
helper.addSnapStorage(accKey, moddedKeys, moddedVals)
|
|
||||||
}
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
t.Logf("Root: %#x\n", root) // Root: 0xca73f6f05ba4ca3024ef340ef3dfca8fdabc1b677ff13f5a9571fd49c16e67ff
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
func incKey(key []byte) []byte {
|
|
||||||
for i := len(key) - 1; i >= 0; i-- {
|
|
||||||
key[i]++
|
|
||||||
if key[i] != 0x0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func decKey(key []byte) []byte {
|
|
||||||
for i := len(key) - 1; i >= 0; i-- {
|
|
||||||
key[i]--
|
|
||||||
if key[i] != 0xff {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func populateDangling(disk ethdb.KeyValueStore) {
|
|
||||||
populate := func(accountHash common.Hash, keys []string, vals []string) {
|
|
||||||
for i, key := range keys {
|
|
||||||
rawdb.WriteStorageSnapshot(disk, accountHash, hashData([]byte(key)), []byte(vals[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Dangling storages of the "first" account
|
|
||||||
populate(common.Hash{}, []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Dangling storages of the "last" account
|
|
||||||
populate(common.HexToHash("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Dangling storages around the account 1
|
|
||||||
hash := decKey(hashData([]byte("acc-1")).Bytes())
|
|
||||||
populate(common.BytesToHash(hash), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
hash = incKey(hashData([]byte("acc-1")).Bytes())
|
|
||||||
populate(common.BytesToHash(hash), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Dangling storages around the account 2
|
|
||||||
hash = decKey(hashData([]byte("acc-2")).Bytes())
|
|
||||||
populate(common.BytesToHash(hash), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
hash = incKey(hashData([]byte("acc-2")).Bytes())
|
|
||||||
populate(common.BytesToHash(hash), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Dangling storages around the account 3
|
|
||||||
hash = decKey(hashData([]byte("acc-3")).Bytes())
|
|
||||||
populate(common.BytesToHash(hash), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
hash = incKey(hashData([]byte("acc-3")).Bytes())
|
|
||||||
populate(common.BytesToHash(hash), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
// Dangling storages of the random account
|
|
||||||
populate(randomHash(), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
populate(randomHash(), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
populate(randomHash(), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation with dangling storages. Dangling storage means
|
|
||||||
// the storage data is existent while the corresponding account data is missing.
|
|
||||||
//
|
|
||||||
// This test will populate some dangling storages to see if they can be cleaned up.
|
|
||||||
func TestGenerateCompleteSnapshotWithDanglingStorage(t *testing.T) {
|
|
||||||
testGenerateCompleteSnapshotWithDanglingStorage(t, rawdb.HashScheme)
|
|
||||||
testGenerateCompleteSnapshotWithDanglingStorage(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateCompleteSnapshotWithDanglingStorage(t *testing.T, scheme string) {
|
|
||||||
var helper = newHelper(scheme)
|
|
||||||
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addAccount("acc-2", &types.StateAccount{Balance: big.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addAccount("acc-3", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
helper.addSnapStorage("acc-1", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
helper.addSnapStorage("acc-3", []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"})
|
|
||||||
|
|
||||||
populateDangling(helper.diskdb)
|
|
||||||
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that snapshot generation with dangling storages. Dangling storage means
|
|
||||||
// the storage data is existent while the corresponding account data is missing.
|
|
||||||
//
|
|
||||||
// This test will populate some dangling storages to see if they can be cleaned up.
|
|
||||||
func TestGenerateBrokenSnapshotWithDanglingStorage(t *testing.T) {
|
|
||||||
testGenerateBrokenSnapshotWithDanglingStorage(t, rawdb.HashScheme)
|
|
||||||
testGenerateBrokenSnapshotWithDanglingStorage(t, rawdb.PathScheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testGenerateBrokenSnapshotWithDanglingStorage(t *testing.T, scheme string) {
|
|
||||||
var helper = newHelper(scheme)
|
|
||||||
|
|
||||||
stRoot := helper.makeStorageTrie(hashData([]byte("acc-1")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount("acc-1", &types.StateAccount{Balance: big.NewInt(1), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
helper.addTrieAccount("acc-2", &types.StateAccount{Balance: big.NewInt(2), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true)
|
|
||||||
helper.addTrieAccount("acc-3", &types.StateAccount{Balance: big.NewInt(3), Root: stRoot, CodeHash: types.EmptyCodeHash.Bytes()})
|
|
||||||
|
|
||||||
populateDangling(helper.diskdb)
|
|
||||||
|
|
||||||
root, snap := helper.CommitAndGenerate()
|
|
||||||
select {
|
|
||||||
case <-snap.genPending:
|
|
||||||
// Snapshot generation succeeded
|
|
||||||
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Errorf("Snapshot generation failed")
|
|
||||||
}
|
|
||||||
checkSnapRoot(t, snap, root)
|
|
||||||
|
|
||||||
// Signal abortion to the generator and wait for it to tear down
|
|
||||||
stop := make(chan *generatorStats)
|
|
||||||
snap.genAbort <- stop
|
|
||||||
<-stop
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
// Copyright 2022 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package snapshot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// holdableIterator is a wrapper of underlying database iterator. It extends
|
|
||||||
// the basic iterator interface by adding Hold which can hold the element
|
|
||||||
// locally where the iterator is currently located and serve it up next time.
|
|
||||||
type holdableIterator struct {
|
|
||||||
it ethdb.Iterator
|
|
||||||
key []byte
|
|
||||||
val []byte
|
|
||||||
atHeld bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// newHoldableIterator initializes the holdableIterator with the given iterator.
|
|
||||||
func newHoldableIterator(it ethdb.Iterator) *holdableIterator {
|
|
||||||
return &holdableIterator{it: it}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hold holds the element locally where the iterator is currently located which
|
|
||||||
// can be served up next time.
|
|
||||||
func (it *holdableIterator) Hold() {
|
|
||||||
if it.it.Key() == nil {
|
|
||||||
return // nothing to hold
|
|
||||||
}
|
|
||||||
it.key = common.CopyBytes(it.it.Key())
|
|
||||||
it.val = common.CopyBytes(it.it.Value())
|
|
||||||
it.atHeld = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next moves the iterator to the next key/value pair. It returns whether the
|
|
||||||
// iterator is exhausted.
|
|
||||||
func (it *holdableIterator) Next() bool {
|
|
||||||
if !it.atHeld && it.key != nil {
|
|
||||||
it.atHeld = true
|
|
||||||
} else if it.atHeld {
|
|
||||||
it.atHeld = false
|
|
||||||
it.key = nil
|
|
||||||
it.val = nil
|
|
||||||
}
|
|
||||||
if it.key != nil {
|
|
||||||
return true // shifted to locally held value
|
|
||||||
}
|
|
||||||
return it.it.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns any accumulated error. Exhausting all the key/value pairs
|
|
||||||
// is not considered to be an error.
|
|
||||||
func (it *holdableIterator) Error() error { return it.it.Error() }
|
|
||||||
|
|
||||||
// Release releases associated resources. Release should always succeed and can
|
|
||||||
// be called multiple times without causing error.
|
|
||||||
func (it *holdableIterator) Release() {
|
|
||||||
it.atHeld = false
|
|
||||||
it.key = nil
|
|
||||||
it.val = nil
|
|
||||||
it.it.Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key returns the key of the current key/value pair, or nil if done. The caller
|
|
||||||
// should not modify the contents of the returned slice, and its contents may
|
|
||||||
// change on the next call to Next.
|
|
||||||
func (it *holdableIterator) Key() []byte {
|
|
||||||
if it.key != nil {
|
|
||||||
return it.key
|
|
||||||
}
|
|
||||||
return it.it.Key()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns the value of the current key/value pair, or nil if done. The
|
|
||||||
// caller should not modify the contents of the returned slice, and its contents
|
|
||||||
// may change on the next call to Next.
|
|
||||||
func (it *holdableIterator) Value() []byte {
|
|
||||||
if it.val != nil {
|
|
||||||
return it.val
|
|
||||||
}
|
|
||||||
return it.it.Value()
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue