From ac227a80fee98e68f1313ff1652bac34288a0d6b Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Mon, 1 Feb 2021 19:00:07 +0300 Subject: [PATCH] stateroot: use RoleStateValidator for verification --- internal/fakechain/fakechain.go | 2 +- pkg/core/blockchain.go | 1 + pkg/core/blockchainer/state_root.go | 4 + pkg/core/native/designate.go | 11 +- pkg/core/stateroot/module.go | 45 +++++++- pkg/core/stateroot/store.go | 39 ++++++- pkg/core/stateroot/validators.go | 37 +++++++ pkg/core/stateroot_test.go | 153 ++++++++++++++++++++++++++++ pkg/network/server.go | 21 +++- pkg/rpc/server/server.go | 2 +- pkg/services/stateroot/message.go | 50 +++++++++ pkg/services/stateroot/service.go | 51 ++++++++++ 12 files changed, 405 insertions(+), 11 deletions(-) create mode 100644 pkg/core/stateroot/validators.go create mode 100644 pkg/core/stateroot_test.go create mode 100644 pkg/services/stateroot/message.go create mode 100644 pkg/services/stateroot/service.go diff --git a/internal/fakechain/fakechain.go b/internal/fakechain/fakechain.go index fae3804a7..f449b36e8 100644 --- a/internal/fakechain/fakechain.go +++ b/internal/fakechain/fakechain.go @@ -276,7 +276,7 @@ func (chain *FakeChain) GetEnrollments() ([]state.Validator, error) { // GetStateModule implements Blockchainer interface. func (chain *FakeChain) GetStateModule() blockchainer.StateRoot { - panic("TODO") + return nil } // GetStorageItem implements Blockchainer interface. diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 998992a12..73dcfe664 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -196,6 +196,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L } bc.stateRoot = stateroot.NewModule(bc, bc.log, bc.dao.Store) + bc.contracts.Designate.StateRootService = bc.stateRoot if err := bc.init(); err != nil { return nil, err diff --git a/pkg/core/blockchainer/state_root.go b/pkg/core/blockchainer/state_root.go index 57c76c69f..e1086edee 100644 --- a/pkg/core/blockchainer/state_root.go +++ b/pkg/core/blockchainer/state_root.go @@ -2,12 +2,16 @@ package blockchainer import ( "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/util" ) // StateRoot represents local state root module. type StateRoot interface { + AddStateRoot(root *state.MPTRoot) error CurrentLocalStateRoot() util.Uint256 + CurrentValidatedHeight() uint32 GetStateProof(root util.Uint256, key []byte) ([][]byte, error) GetStateRoot(height uint32) (*state.MPTRoot, error) + UpdateStateValidators(height uint32, pubs keys.PublicKeys) } diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index c7d225861..44e40978d 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -7,6 +7,7 @@ import ( "sort" "sync/atomic" + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer/services" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -40,6 +41,8 @@ type Designate struct { OracleService atomic.Value // NotaryService represents Notary node module. NotaryService atomic.Value + // StateRootService represents StateRoot node module. + StateRootService blockchainer.StateRoot } type roleData struct { @@ -172,12 +175,10 @@ func (s *Designate) hashFromNodes(r Role, nodes keys.PublicKeys) util.Uint160 { } var script []byte switch r { - case RoleOracle, RoleNeoFSAlphabet: - script, _ = smartcontract.CreateDefaultMultiSigRedeemScript(nodes.Copy()) case RoleP2PNotary: script, _ = smartcontract.CreateMultiSigRedeemScript(1, nodes.Copy()) default: - script, _ = smartcontract.CreateMajorityMultiSigRedeemScript(nodes.Copy()) + script, _ = smartcontract.CreateDefaultMultiSigRedeemScript(nodes.Copy()) } return hash.Hash160(script) } @@ -201,6 +202,10 @@ func (s *Designate) updateCachedRoleData(v *atomic.Value, d dao.DAO, r Role) err if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil { ntr.UpdateNotaryNodes(nodeKeys.Copy()) } + case RoleStateValidator: + if s.StateRootService != nil { + s.StateRootService.UpdateStateValidators(height, nodeKeys.Copy()) + } } return nil } diff --git a/pkg/core/stateroot/module.go b/pkg/core/stateroot/module.go index 9bf646fca..477e040cb 100644 --- a/pkg/core/stateroot/module.go +++ b/pkg/core/stateroot/module.go @@ -1,12 +1,16 @@ package stateroot import ( + "encoding/binary" + "errors" "fmt" + "sync" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "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/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/util" "go.uber.org/atomic" "go.uber.org/zap" @@ -20,9 +24,19 @@ type ( bc blockchainer.Blockchainer log *zap.Logger + currentLocal atomic.Value localHeight atomic.Uint32 validatedHeight atomic.Uint32 - currentLocal atomic.Value + + mtx sync.RWMutex + keys []keyCache + } + + keyCache struct { + height uint32 + validatorsKeys keys.PublicKeys + validatorsHash util.Uint160 + validatorsScript []byte } ) @@ -51,8 +65,18 @@ func (s *Module) CurrentLocalStateRoot() util.Uint256 { return s.currentLocal.Load().(util.Uint256) } +// CurrentValidatedHeight returns current state root validated height. +func (s *Module) CurrentValidatedHeight() uint32 { + return s.validatedHeight.Load() +} + // Init initializes state root module at the given height. func (s *Module) Init(height uint32, enableRefCount bool) error { + data, err := s.Store.Get([]byte{byte(storage.DataMPT), prefixValidated}) + if err == nil { + s.validatedHeight.Store(binary.LittleEndian.Uint32(data)) + } + var gcKey = []byte{byte(storage.DataMPT), prefixGC} if height == 0 { s.mpt = mpt.NewTrie(nil, enableRefCount, s.Store) @@ -96,3 +120,22 @@ func (s *Module) AddMPTBatch(index uint32, b mpt.Batch) error { _, err = s.Store.Persist() return err } + +// VerifyStateRoot checks if state root is valid. +func (s *Module) VerifyStateRoot(r *state.MPTRoot) error { + _, err := s.getStateRoot(makeStateRootKey(r.Index - 1)) + if err != nil { + return errors.New("can't get previous state root") + } + return s.verifyWitness(r) +} + +const maxVerificationGAS = 1_00000000 + +// verifyWitness verifies state root witness. +func (s *Module) verifyWitness(r *state.MPTRoot) error { + s.mtx.Lock() + h := s.getKeyCacheForHeight(r.Index).validatorsHash + s.mtx.Unlock() + return s.bc.VerifyWitness(h, r, r.Witness, maxVerificationGAS) +} diff --git a/pkg/core/stateroot/store.go b/pkg/core/stateroot/store.go index cc43bd0cf..b2ab7d210 100644 --- a/pkg/core/stateroot/store.go +++ b/pkg/core/stateroot/store.go @@ -9,8 +9,9 @@ import ( ) const ( - prefixGC = 0x01 - prefixLocal = 0x02 + prefixGC = 0x01 + prefixLocal = 0x02 + prefixValidated = 0x03 ) func (s *Module) addLocalStateRoot(sr *state.MPTRoot) error { @@ -26,7 +27,10 @@ func (s *Module) addLocalStateRoot(sr *state.MPTRoot) error { } s.currentLocal.Store(sr.Root) s.localHeight.Store(sr.Index) - updateStateHeightMetric(sr.Index) + if s.bc.GetConfig().StateRootInHeader { + s.validatedHeight.Store(sr.Index) + updateStateHeightMetric(sr.Index) + } return nil } @@ -54,3 +58,32 @@ func makeStateRootKey(index uint32) []byte { binary.BigEndian.PutUint32(key, index) return key } + +// AddStateRoot adds validated state root provided by network. +func (s *Module) AddStateRoot(sr *state.MPTRoot) error { + if err := s.VerifyStateRoot(sr); err != nil { + return err + } + key := makeStateRootKey(sr.Index) + local, err := s.getStateRoot(key) + if err != nil { + return err + } + if local.Witness != nil { + return nil + } + if err := s.putStateRoot(key, sr); err != nil { + return err + } + + data := make([]byte, 4) + binary.LittleEndian.PutUint32(data, sr.Index) + if err := s.Store.Put([]byte{byte(storage.DataMPT), prefixValidated}, data); err != nil { + return err + } + s.validatedHeight.Store(sr.Index) + if !s.bc.GetConfig().StateRootInHeader { + updateStateHeightMetric(sr.Index) + } + return nil +} diff --git a/pkg/core/stateroot/validators.go b/pkg/core/stateroot/validators.go new file mode 100644 index 000000000..58348091a --- /dev/null +++ b/pkg/core/stateroot/validators.go @@ -0,0 +1,37 @@ +package stateroot + +import ( + "sort" + + "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/smartcontract" +) + +// UpdateStateValidators updates list of state validator keys. +func (s *Module) UpdateStateValidators(height uint32, pubs keys.PublicKeys) { + script, _ := smartcontract.CreateDefaultMultiSigRedeemScript(pubs) + h := hash.Hash160(script) + + s.mtx.Lock() + kc := s.getKeyCacheForHeight(height) + if kc.validatorsHash != h { + s.keys = append(s.keys, keyCache{ + height: height, + validatorsKeys: pubs, + validatorsHash: h, + validatorsScript: script, + }) + } + s.mtx.Unlock() +} + +func (s *Module) getKeyCacheForHeight(h uint32) keyCache { + index := sort.Search(len(s.keys), func(i int) bool { + return s.keys[i].height >= h + }) + if index == len(s.keys) { + return keyCache{} + } + return s.keys[index] +} diff --git a/pkg/core/stateroot_test.go b/pkg/core/stateroot_test.go new file mode 100644 index 000000000..163ae2445 --- /dev/null +++ b/pkg/core/stateroot_test.go @@ -0,0 +1,153 @@ +package core + +import ( + "errors" + "sort" + "testing" + + "github.com/nspcc-dev/neo-go/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "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/network/payload" + "github.com/nspcc-dev/neo-go/pkg/services/stateroot" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func testSignStateRoot(t *testing.T, r *state.MPTRoot, pubs keys.PublicKeys, accs ...*wallet.Account) []byte { + n := smartcontract.GetMajorityHonestNodeCount(len(accs)) + w := io.NewBufBinWriter() + for i := 0; i < n; i++ { + sig := accs[i].PrivateKey().SignHash(r.GetSignedHash()) + emit.Bytes(w.BinWriter, sig) + } + require.NoError(t, w.Err) + + script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs.Copy()) + require.NoError(t, err) + r.Witness = &transaction.Witness{ + VerificationScript: script, + InvocationScript: w.Bytes(), + } + data, err := testserdes.EncodeBinary(stateroot.NewMessage(stateroot.RootT, r)) + require.NoError(t, err) + return data +} + +func newMajorityMultisigWithGAS(t *testing.T, n int) (util.Uint160, keys.PublicKeys, []*wallet.Account) { + accs := make([]*wallet.Account, n) + for i := range accs { + acc, err := wallet.NewAccount() + require.NoError(t, err) + accs[i] = acc + } + sort.Slice(accs, func(i, j int) bool { + pi := accs[i].PrivateKey().PublicKey() + pj := accs[j].PrivateKey().PublicKey() + return pi.Cmp(pj) == -1 + }) + pubs := make(keys.PublicKeys, n) + for i := range pubs { + pubs[i] = accs[i].PrivateKey().PublicKey() + } + script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs) + require.NoError(t, err) + return hash.Hash160(script), pubs, accs +} + +func TestStateRoot(t *testing.T) { + bc := newTestChain(t) + + h, pubs, accs := newMajorityMultisigWithGAS(t, 2) + bc.setNodesByRole(t, true, native.RoleStateValidator, pubs) + updateIndex := bc.BlockHeight() + transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + + srv, err := stateroot.New(bc.GetStateModule()) + require.NoError(t, err) + require.EqualValues(t, 0, srv.CurrentValidatedHeight()) + r, err := srv.GetStateRoot(bc.BlockHeight()) + require.NoError(t, err) + require.Equal(t, r.Root, srv.CurrentLocalStateRoot()) + + t.Run("invalid message", func(t *testing.T) { + require.Error(t, srv.OnPayload(&payload.Extensible{Data: []byte{42}})) + require.EqualValues(t, 0, srv.CurrentValidatedHeight()) + }) + t.Run("drop zero index", func(t *testing.T) { + r, err := srv.GetStateRoot(0) + require.NoError(t, err) + data, err := testserdes.EncodeBinary(stateroot.NewMessage(stateroot.RootT, r)) + require.NoError(t, err) + require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) + require.EqualValues(t, 0, srv.CurrentValidatedHeight()) + }) + t.Run("invalid height", func(t *testing.T) { + r, err := srv.GetStateRoot(1) + require.NoError(t, err) + r.Index = 10 + data := testSignStateRoot(t, r, pubs, accs...) + require.Error(t, srv.OnPayload(&payload.Extensible{Data: data})) + require.EqualValues(t, 0, srv.CurrentValidatedHeight()) + }) + t.Run("invalid signer", func(t *testing.T) { + accInv, err := wallet.NewAccount() + require.NoError(t, err) + pubs := keys.PublicKeys{accInv.PrivateKey().PublicKey()} + require.NoError(t, accInv.ConvertMultisig(1, pubs)) + transferTokenFromMultisigAccount(t, bc, accInv.Contract.ScriptHash(), bc.contracts.GAS.Hash, 1_0000_0000) + r, err := srv.GetStateRoot(1) + require.NoError(t, err) + data := testSignStateRoot(t, r, pubs, accInv) + err = srv.OnPayload(&payload.Extensible{Data: data}) + require.True(t, errors.Is(err, ErrWitnessHashMismatch), "got: %v", err) + require.EqualValues(t, 0, srv.CurrentValidatedHeight()) + }) + + r, err = srv.GetStateRoot(updateIndex + 1) + require.NoError(t, err) + data := testSignStateRoot(t, r, pubs, accs...) + require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) + require.EqualValues(t, 2, srv.CurrentValidatedHeight()) + + r, err = srv.GetStateRoot(updateIndex + 1) + require.NoError(t, err) + require.NotNil(t, r.Witness) + require.Equal(t, h, r.Witness.ScriptHash()) +} + +func TestStateRootInitNonZeroHeight(t *testing.T) { + st := memoryStore{storage.NewMemoryStore()} + h, pubs, accs := newMajorityMultisigWithGAS(t, 2) + + var root util.Uint256 + t.Run("init", func(t *testing.T) { // this is in a separate test to do proper cleanup + bc := newTestChainWithCustomCfgAndStore(t, st, nil) + bc.setNodesByRole(t, true, native.RoleStateValidator, pubs) + transferTokenFromMultisigAccount(t, bc, h, bc.contracts.GAS.Hash, 1_0000_0000) + + _, err := persistBlock(bc) + require.NoError(t, err) + srv, err := stateroot.New(bc.GetStateModule()) + require.NoError(t, err) + r, err := srv.GetStateRoot(2) + require.NoError(t, err) + data := testSignStateRoot(t, r, pubs, accs...) + require.NoError(t, srv.OnPayload(&payload.Extensible{Data: data})) + require.EqualValues(t, 2, srv.CurrentValidatedHeight()) + root = srv.CurrentLocalStateRoot() + }) + + bc2 := newTestChainWithCustomCfgAndStore(t, st, nil) + srv := bc2.GetStateModule() + require.EqualValues(t, 2, srv.CurrentValidatedHeight()) + require.Equal(t, root, srv.CurrentLocalStateRoot()) +} diff --git a/pkg/network/server.go b/pkg/network/server.go index 89b3bc302..0d594c42a 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -22,6 +22,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/services/notary" "github.com/nspcc-dev/neo-go/pkg/services/oracle" + "github.com/nspcc-dev/neo-go/pkg/services/stateroot" "github.com/nspcc-dev/neo-go/pkg/util" "go.uber.org/atomic" "go.uber.org/zap" @@ -87,7 +88,8 @@ type ( consensusStarted *atomic.Bool canHandleExtens *atomic.Bool - oracle *oracle.Oracle + oracle *oracle.Oracle + stateRoot stateroot.Service log *zap.Logger } @@ -171,6 +173,12 @@ func newServerFromConstructors(config ServerConfig, chain blockchainer.Blockchai } }) + sr, err := stateroot.New(chain.GetStateModule()) + if err != nil { + return nil, fmt.Errorf("can't initialize StateRoot service: %w", err) + } + s.stateRoot = sr + if config.OracleCfg.Enabled { orcCfg := oracle.Config{ Log: log, @@ -295,6 +303,11 @@ func (s *Server) GetOracle() *oracle.Oracle { return s.oracle } +// GetStateRoot returns state root service instance. +func (s *Server) GetStateRoot() stateroot.Service { + return s.stateRoot +} + // UnconnectedPeers returns a list of peers that are in the discovery peer list // but are not connected to the server. func (s *Server) UnconnectedPeers() []string { @@ -803,7 +816,11 @@ func (s *Server) handleExtensibleCmd(e *payload.Extensible) error { switch e.Category { case consensus.Category: s.consensus.OnPayload(e) - case "StateService": // no-op for now + case stateroot.Category: + err := s.stateRoot.OnPayload(e) + if err != nil { + return err + } default: return errors.New("invalid category") } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index fbc1d4a99..b193746f4 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -869,7 +869,7 @@ func (s *Server) verifyProof(ps request.Params) (interface{}, *response.Error) { func (s *Server) getStateHeight(_ request.Params) (interface{}, *response.Error) { var height = s.chain.BlockHeight() - var stateHeight uint32 + var stateHeight = s.chain.GetStateModule().CurrentValidatedHeight() if s.chain.GetConfig().StateRootInHeader { stateHeight = height - 1 } diff --git a/pkg/services/stateroot/message.go b/pkg/services/stateroot/message.go new file mode 100644 index 000000000..c9f603a8a --- /dev/null +++ b/pkg/services/stateroot/message.go @@ -0,0 +1,50 @@ +package stateroot + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/io" +) + +type ( + // MessageType represents message type. + MessageType byte + + // Message represents state-root related message. + Message struct { + Type MessageType + Payload io.Serializable + } +) + +// Various message types. +const ( + RootT MessageType = 1 +) + +// NewMessage creates new message of specified type. +func NewMessage(typ MessageType, p io.Serializable) *Message { + return &Message{ + Type: typ, + Payload: p, + } +} + +// EncodeBinary implements io.Serializable interface. +func (m *Message) EncodeBinary(w *io.BinWriter) { + w.WriteB(byte(m.Type)) + m.Payload.EncodeBinary(w) +} + +// DecodeBinary implements io.Serializable interface. +func (m *Message) DecodeBinary(r *io.BinReader) { + switch m.Type = MessageType(r.ReadB()); m.Type { + case RootT: + m.Payload = new(state.MPTRoot) + default: + r.Err = fmt.Errorf("invalid type: %x", m.Type) + return + } + m.Payload.DecodeBinary(r) +} diff --git a/pkg/services/stateroot/service.go b/pkg/services/stateroot/service.go new file mode 100644 index 000000000..7dfc49bb7 --- /dev/null +++ b/pkg/services/stateroot/service.go @@ -0,0 +1,51 @@ +package stateroot + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/network/payload" +) + +type ( + // Service represents state root service. + Service interface { + blockchainer.StateRoot + OnPayload(p *payload.Extensible) error + } + + service struct { + blockchainer.StateRoot + } +) + +const ( + // Category is message category for extensible payloads. + Category = "StateService" +) + +// New returns new state root service instance using underlying module. +func New(mod blockchainer.StateRoot) (Service, error) { + return &service{ + StateRoot: mod, + }, nil +} + +// OnPayload implements Service interface. +func (s *service) OnPayload(ep *payload.Extensible) error { + m := new(Message) + r := io.NewBinReaderFromBuf(ep.Data) + m.DecodeBinary(r) + if r.Err != nil { + return r.Err + } + switch m.Type { + case RootT: + sr := m.Payload.(*state.MPTRoot) + if sr.Index == 0 { + return nil + } + return s.AddStateRoot(sr) + } + return nil +}