core: update MPT during block processing

This commit is contained in:
Evgenii Stratonikov 2020-05-29 17:20:00 +03:00
parent 2f90a06db3
commit 20f190ef69
7 changed files with 338 additions and 6 deletions

View file

@ -16,6 +16,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
@ -229,6 +230,9 @@ func (bc *Blockchain) init() error {
}
bc.blockHeight = bHeight
bc.persistedHeight = bHeight
if err = bc.dao.InitMPT(bHeight); err != nil {
return errors.Wrapf(err, "can't init MPT at height %d", bHeight)
}
hashes, err := bc.dao.GetHeaderHashes()
if err != nil {
@ -551,6 +555,11 @@ func (bc *Blockchain) getSystemFeeAmount(h util.Uint256) uint32 {
return sf
}
// GetStateRoot returns state root for a given height.
func (bc *Blockchain) GetStateRoot(height uint32) (*state.MPTRootState, error) {
return bc.dao.GetStateRoot(height)
}
// TODO: storeBlock needs some more love, its implemented as in the original
// project. This for the sake of development speed and understanding of what
// is happening here, quite allot as you can see :). If things are wired together
@ -819,16 +828,37 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
}
}
root := bc.dao.MPT.StateRoot()
var prevHash util.Uint256
if block.Index > 0 {
prev, err := bc.dao.GetStateRoot(block.Index - 1)
if err != nil {
return errors.WithMessagef(err, "can't get previous state root")
}
prevHash = prev.Root
}
err := bc.AddStateRoot(&state.MPTRoot{
MPTRootBase: state.MPTRootBase{
Index: block.Index,
PrevHash: prevHash,
Root: root,
},
})
if err != nil {
return err
}
if bc.config.SaveStorageBatch {
bc.lastBatch = cache.DAO.GetBatch()
}
bc.lock.Lock()
_, err := cache.Persist()
_, err = cache.Persist()
if err != nil {
bc.lock.Unlock()
return err
}
bc.dao.MPT.Flush()
bc.topBlock.Store(block)
atomic.StoreUint32(&bc.blockHeight, block.Index)
bc.memPool.RemoveStale(bc.isTxStillRelevant)
@ -1732,6 +1762,65 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction) bool {
}
// AddStateRoot add new (possibly unverified) state root to the blockchain.
func (bc *Blockchain) AddStateRoot(r *state.MPTRoot) error {
our, err := bc.GetStateRoot(r.Index)
if err == nil {
if our.Flag == state.Verified {
return nil
} else if r.Witness == nil && our.Witness != nil {
r.Witness = our.Witness
}
}
if err := bc.verifyStateRoot(r); err != nil {
return errors.WithMessage(err, "invalid state root")
}
if r.Index > bc.BlockHeight() { // just put it into the store for future checks
return bc.dao.PutStateRoot(&state.MPTRootState{
MPTRoot: *r,
Flag: state.Unverified,
})
}
flag := state.Unverified
if r.Witness != nil {
if err := bc.verifyStateRootWitness(r); err != nil {
return errors.WithMessage(err, "can't verify signature")
}
flag = state.Verified
}
return bc.dao.PutStateRoot(&state.MPTRootState{
MPTRoot: *r,
Flag: flag,
})
}
// verifyStateRoot checks if state root is valid.
func (bc *Blockchain) verifyStateRoot(r *state.MPTRoot) error {
if r.Index == 0 {
return nil
}
prev, err := bc.GetStateRoot(r.Index - 1)
if err != nil {
return errors.New("can't get previous state root")
} else if !prev.Root.Equals(r.PrevHash) {
return errors.New("previous hash mismatch")
} else if prev.Version != r.Version {
return errors.New("version mismatch")
}
return nil
}
// verifyStateRootWitness verifies that state root signature is correct.
func (bc *Blockchain) verifyStateRootWitness(r *state.MPTRoot) error {
b, err := bc.GetBlock(bc.GetHeaderHash(int(r.Index)))
if err != nil {
return err
}
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil)
return bc.verifyHashAgainstScript(b.NextConsensus, r.Witness, hash.Sha256(r.GetSignedPart()), interopCtx, true)
}
// VerifyTx verifies whether a transaction is bonafide or not. Block parameter
// is used for easy interop access and can be omitted for transactions that are
// not yet added into any block.

View file

@ -18,6 +18,7 @@ type Blockchainer interface {
GetConfig() config.ProtocolConfiguration
AddHeaders(...*block.Header) error
AddBlock(*block.Block) error
AddStateRoot(r *state.MPTRoot) error
BlockHeight() uint32
CalculateClaimable(value util.Fixed8, startHeight, endHeight uint32) (util.Fixed8, util.Fixed8, error)
Close()
@ -38,6 +39,7 @@ type Blockchainer interface {
GetNEP5Balances(util.Uint160) *state.NEP5Balances
GetValidators(txes ...*transaction.Transaction) ([]*keys.PublicKey, error)
GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error)
GetStateRoot(height uint32) (*state.MPTRootState, error)
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem
GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error)
GetTestVM() *vm.VM

View file

@ -7,6 +7,7 @@ import (
"sort"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/mpt"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -34,6 +35,8 @@ type DAO interface {
GetHeaderHashes() ([]util.Uint256, error)
GetNEP5Balances(acc util.Uint160) (*state.NEP5Balances, error)
GetNEP5TransferLog(acc util.Uint160, index uint32) (*state.NEP5TransferLog, error)
GetStateRoot(height uint32) (*state.MPTRootState, error)
PutStateRoot(root *state.MPTRootState) error
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem
GetStorageItems(hash util.Uint160, prefix []byte) ([]StorageItemWithKey, error)
GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error)
@ -70,12 +73,14 @@ type DAO interface {
// Simple is memCached wrapper around DB, simple DAO implementation.
type Simple struct {
MPT *mpt.Trie
Store *storage.MemCachedStore
}
// NewSimple creates new simple dao using provided backend store.
func NewSimple(backend storage.Store) *Simple {
return &Simple{Store: storage.NewMemCachedStore(backend)}
st := storage.NewMemCachedStore(backend)
return &Simple{Store: st, MPT: mpt.NewTrie(nil, st)}
}
// GetBatch returns currently accumulated DB changeset.
@ -86,7 +91,9 @@ func (dao *Simple) GetBatch() *storage.MemBatch {
// GetWrapped returns new DAO instance with another layer of wrapped
// MemCachedStore around the current DAO Store.
func (dao *Simple) GetWrapped() DAO {
return NewSimple(dao.Store)
d := NewSimple(dao.Store)
d.MPT = dao.MPT
return d
}
// GetAndDecode performs get operation and decoding with serializable structures.
@ -406,6 +413,42 @@ func (dao *Simple) PutAppExecResult(aer *state.AppExecResult) error {
// -- start storage item.
func makeStateRootKey(height uint32) []byte {
key := make([]byte, 5)
key[0] = byte(storage.DataMPT)
binary.LittleEndian.PutUint32(key[1:], height)
return key
}
// InitMPT initializes MPT at the given height.
func (dao *Simple) InitMPT(height uint32) error {
if height == 0 {
dao.MPT = mpt.NewTrie(nil, dao.Store)
return nil
}
r, err := dao.GetStateRoot(height)
if err != nil {
return err
}
dao.MPT = mpt.NewTrie(mpt.NewHashNode(r.Root), dao.Store)
return nil
}
// GetStateRoot returns state root of a given height.
func (dao *Simple) GetStateRoot(height uint32) (*state.MPTRootState, error) {
r := new(state.MPTRootState)
err := dao.GetAndDecode(r, makeStateRootKey(height))
if err != nil {
return nil, err
}
return r, nil
}
// PutStateRoot puts state root of a given height into the store.
func (dao *Simple) PutStateRoot(r *state.MPTRootState) error {
return dao.Put(r, makeStateRootKey(r.Index))
}
// GetStorageItem returns StorageItem if it exists in the given store.
func (dao *Simple) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem {
b, err := dao.Store.Get(makeStorageItemKey(scripthash, key))
@ -426,13 +469,24 @@ func (dao *Simple) GetStorageItem(scripthash util.Uint160, key []byte) *state.St
// PutStorageItem puts given StorageItem for given script with given
// key into the given store.
func (dao *Simple) PutStorageItem(scripthash util.Uint160, key []byte, si *state.StorageItem) error {
return dao.Put(si, makeStorageItemKey(scripthash, key))
stKey := makeStorageItemKey(scripthash, key)
k := mpt.ToNeoStorageKey(stKey[1:]) // strip STStorage prefix
v := mpt.ToNeoStorageValue(si)
if err := dao.MPT.Put(k, v); err != nil && err != mpt.ErrNotFound {
return err
}
return dao.Put(si, stKey)
}
// DeleteStorageItem drops storage item for the given script with the
// given key from the store.
func (dao *Simple) DeleteStorageItem(scripthash util.Uint160, key []byte) error {
return dao.Store.Delete(makeStorageItemKey(scripthash, key))
stKey := makeStorageItemKey(scripthash, key)
k := mpt.ToNeoStorageKey(stKey[1:]) // strip STStorage prefix
if err := dao.MPT.Delete(k); err != nil && err != mpt.ErrNotFound {
return err
}
return dao.Store.Delete(stKey)
}
// StorageItemWithKey is a Key-Value pair together with possible const modifier.

View file

@ -1,6 +1,10 @@
package mpt
import "github.com/nspcc-dev/neo-go/pkg/util"
import (
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// lcp returns longest common prefix of a and b.
// Note: it does no allocations.
@ -69,3 +73,14 @@ func ToNeoStorageKey(key []byte) []byte {
}
return append(nkey, byte(padding))
}
// ToNeoStorageValue serializes si to a C# neo node's format.
// It has additional version (0x00) byte at the beginning.
func ToNeoStorageValue(si *state.StorageItem) []byte {
const version = 0
buf := io.NewBufBinWriter()
buf.BinWriter.WriteB(version)
si.EncodeBinary(buf.BinWriter)
return buf.Bytes()
}

105
pkg/core/state/mpt_root.go Normal file
View file

@ -0,0 +1,105 @@
package state
import (
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// MPTRootBase represents storage state root.
type MPTRootBase struct {
Version byte
Index uint32
PrevHash util.Uint256
Root util.Uint256
}
// MPTRoot represents storage state root together with sign info.
type MPTRoot struct {
MPTRootBase
Witness *transaction.Witness
}
// MPTRootStateFlag represents verification state of the state root.
type MPTRootStateFlag byte
// Possible verification states of MPTRoot.
const (
Unverified MPTRootStateFlag = 0x00
Verified MPTRootStateFlag = 0x01
Invalid MPTRootStateFlag = 0x03
)
// MPTRootState represents state root together with its verification state.
type MPTRootState struct {
MPTRoot
Flag MPTRootStateFlag
}
// EncodeBinary implements io.Serializable.
func (s *MPTRootState) EncodeBinary(w *io.BinWriter) {
w.WriteB(byte(s.Flag))
s.MPTRoot.EncodeBinary(w)
}
// DecodeBinary implements io.Serializable.
func (s *MPTRootState) DecodeBinary(r *io.BinReader) {
s.Flag = MPTRootStateFlag(r.ReadB())
s.MPTRoot.DecodeBinary(r)
}
// GetSignedPart returns part of MPTRootBase which needs to be signed.
func (s *MPTRootBase) GetSignedPart() []byte {
buf := io.NewBufBinWriter()
s.EncodeBinary(buf.BinWriter)
return buf.Bytes()
}
// Equals checks if s == other.
func (s *MPTRootBase) Equals(other *MPTRootBase) bool {
return s.Version == other.Version && s.Index == other.Index &&
s.PrevHash.Equals(other.PrevHash) && s.Root.Equals(other.Root)
}
// Hash returns hash of s.
func (s *MPTRootBase) Hash() util.Uint256 {
return hash.DoubleSha256(s.GetSignedPart())
}
// DecodeBinary implements io.Serializable.
func (s *MPTRootBase) DecodeBinary(r *io.BinReader) {
s.Version = r.ReadB()
s.Index = r.ReadU32LE()
s.PrevHash.DecodeBinary(r)
s.Root.DecodeBinary(r)
}
// EncodeBinary implements io.Serializable.
func (s *MPTRootBase) EncodeBinary(w *io.BinWriter) {
w.WriteB(s.Version)
w.WriteU32LE(s.Index)
s.PrevHash.EncodeBinary(w)
s.Root.EncodeBinary(w)
}
// DecodeBinary implements io.Serializable.
func (s *MPTRoot) DecodeBinary(r *io.BinReader) {
s.MPTRootBase.DecodeBinary(r)
var ws []transaction.Witness
r.ReadArray(&ws, 1)
if len(ws) == 1 {
s.Witness = &ws[0]
}
}
// EncodeBinary implements io.Serializable.
func (s *MPTRoot) EncodeBinary(w *io.BinWriter) {
s.MPTRootBase.EncodeBinary(w)
if s.Witness == nil {
w.WriteVarUint(0)
} else {
w.WriteArray([]*transaction.Witness{s.Witness})
}
}

View file

@ -0,0 +1,61 @@
package state
import (
"math/rand"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/internal/random"
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
"github.com/stretchr/testify/require"
)
func testStateRoot() *MPTRoot {
return &MPTRoot{
MPTRootBase: MPTRootBase{
Version: byte(rand.Uint32()),
Index: rand.Uint32(),
PrevHash: random.Uint256(),
Root: random.Uint256(),
},
}
}
func TestStateRoot_Serializable(t *testing.T) {
r := testStateRoot()
testserdes.EncodeDecodeBinary(t, r, new(MPTRoot))
t.Run("WithWitness", func(t *testing.T) {
r.Witness = &transaction.Witness{
InvocationScript: random.Bytes(10),
VerificationScript: random.Bytes(11),
}
testserdes.EncodeDecodeBinary(t, r, new(MPTRoot))
})
}
func TestStateRootEquals(t *testing.T) {
r1 := testStateRoot()
r2 := *r1
require.True(t, r1.Equals(&r2.MPTRootBase))
r2.MPTRootBase.Index++
require.False(t, r1.Equals(&r2.MPTRootBase))
}
func TestMPTRootState_Serializable(t *testing.T) {
rs := &MPTRootState{
MPTRoot: *testStateRoot(),
Flag: 0x04,
}
rs.MPTRoot.Witness = &transaction.Witness{
InvocationScript: random.Bytes(10),
VerificationScript: random.Bytes(11),
}
testserdes.EncodeDecodeBinary(t, rs, new(MPTRootState))
}
func TestMPTRootStateUnverifiedByDefault(t *testing.T) {
var r MPTRootState
require.Equal(t, Unverified, r.Flag)
}

View file

@ -59,6 +59,9 @@ func (chain *testChain) AddBlock(block *block.Block) error {
}
return nil
}
func (chain *testChain) AddStateRoot(r *state.MPTRoot) error {
panic("TODO")
}
func (chain *testChain) BlockHeight() uint32 {
return atomic.LoadUint32(&chain.blockheight)
}
@ -105,6 +108,9 @@ func (chain testChain) GetEnrollments() ([]*state.Validator, error) {
func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) {
panic("TODO")
}
func (chain testChain) GetStateRoot(height uint32) (*state.MPTRootState, error) {
panic("TODO")
}
func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem {
panic("TODO")
}