diff --git a/go.mod b/go.mod index 02aa348fb..c598572c3 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/hashicorp/golang-lru v0.5.4 github.com/mr-tron/base58 v1.1.2 - github.com/nspcc-dev/dbft v0.0.0-20201221101812-e13a1a1c3cb2 + github.com/nspcc-dev/dbft v0.0.0-20210122071512-d9a728094f0d github.com/nspcc-dev/rfc6979 v0.2.0 github.com/pierrec/lz4 v2.5.2+incompatible github.com/prometheus/client_golang v1.2.1 diff --git a/go.sum b/go.sum index 636af42ce..4a700eba3 100644 --- a/go.sum +++ b/go.sum @@ -166,8 +166,8 @@ github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a h1:ajvxgEe9qY4vvoSm github.com/nspcc-dev/dbft v0.0.0-20200117124306-478e5cfbf03a/go.mod h1:/YFK+XOxxg0Bfm6P92lY5eDSLYfp06XOdL8KAVgXjVk= github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1 h1:yEx9WznS+rjE0jl0dLujCxuZSIb+UTjF+005TJu/nNI= github.com/nspcc-dev/dbft v0.0.0-20200219114139-199d286ed6c1/go.mod h1:O0qtn62prQSqizzoagHmuuKoz8QMkU3SzBoKdEvm3aQ= -github.com/nspcc-dev/dbft v0.0.0-20201221101812-e13a1a1c3cb2 h1:vbPjd6xbX8w61abcNfzUvSI7WT0QeS9fHWp1Mocv9N0= -github.com/nspcc-dev/dbft v0.0.0-20201221101812-e13a1a1c3cb2/go.mod h1:I5D0W3tu3epdt2RMCTxS//HDr4S+OHRqajouQTOAHI8= +github.com/nspcc-dev/dbft v0.0.0-20210122071512-d9a728094f0d h1:uUaRysqa/9VtHETVARUlteqfbXAgwxR2nvUc4DzK4pI= +github.com/nspcc-dev/dbft v0.0.0-20210122071512-d9a728094f0d/go.mod h1:I5D0W3tu3epdt2RMCTxS//HDr4S+OHRqajouQTOAHI8= github.com/nspcc-dev/neo-go v0.73.1-pre.0.20200303142215-f5a1b928ce09/go.mod h1:pPYwPZ2ks+uMnlRLUyXOpLieaDQSEaf4NM3zHVbRjmg= github.com/nspcc-dev/neofs-crypto v0.2.0 h1:ftN+59WqxSWz/RCgXYOfhmltOOqU+udsNQSvN6wkFck= github.com/nspcc-dev/neofs-crypto v0.2.0/go.mod h1:F/96fUzPM3wR+UGsPi3faVNmFlA9KAEAUQR7dMxZmNA= diff --git a/pkg/consensus/cache_test.go b/pkg/consensus/cache_test.go index a1069686f..d54ed9cf1 100644 --- a/pkg/consensus/cache_test.go +++ b/pkg/consensus/cache_test.go @@ -52,12 +52,12 @@ func getDifferentPayloads(t *testing.T, n int) (payloads []Payload) { var sign [signatureSize]byte random.Fill(sign[:]) - payloads[i].message = &message{} payloads[i].SetValidatorIndex(uint16(i)) payloads[i].SetType(payload.MessageType(commitType)) payloads[i].payload = &commit{ signature: sign, } + payloads[i].encodeData() } return diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index ebf8d4d25..1f6649194 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -21,6 +21,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" + npayload "github.com/nspcc-dev/neo-go/pkg/network/payload" "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" @@ -39,6 +40,9 @@ const defaultTimePerBlock = 15 * time.Second // Number of nanoseconds in millisecond. const nsInMs = 1000000 +// Category is message category for extensible payloads. +const Category = "Consensus" + // Service represents consensus instance. type Service interface { // Start initializes dBFT and starts event loop for consensus service. @@ -48,11 +52,11 @@ type Service interface { Shutdown() // OnPayload is a callback to notify Service about new received payload. - OnPayload(p *Payload) + OnPayload(p *npayload.Extensible) // OnTransaction is a callback to notify Service about new received transaction. OnTransaction(tx *transaction.Transaction) // GetPayload returns Payload with specified hash if it is present in the local cache. - GetPayload(h util.Uint256) *Payload + GetPayload(h util.Uint256) *npayload.Extensible } type service struct { @@ -94,7 +98,7 @@ type Config struct { Logger *zap.Logger // Broadcast is a callback which is called to notify server // about new consensus payload to sent. - Broadcast func(p *Payload) + Broadcast func(p *npayload.Extensible) // Chain is a core.Blockchainer instance. Chain blockchainer.Blockchainer // RequestTx is a callback to which will be called @@ -204,15 +208,33 @@ var ( // NewPayload creates new consensus payload for the provided network. func NewPayload(m netmode.Magic, stateRootEnabled bool) *Payload { return &Payload{ - network: m, - message: &message{ + Extensible: npayload.Extensible{ + Network: m, + Category: Category, + }, + message: message{ stateRootEnabled: stateRootEnabled, }, } } -func (s *service) newPayload() payload.ConsensusPayload { - return NewPayload(s.network, s.stateRootEnabled) +func (s *service) newPayload(c *dbft.Context, t payload.MessageType, msg interface{}) payload.ConsensusPayload { + cp := NewPayload(s.network, s.stateRootEnabled) + cp.SetHeight(c.BlockIndex) + cp.SetValidatorIndex(uint16(c.MyIndex)) + cp.SetViewNumber(c.ViewNumber) + cp.SetType(t) + if pr, ok := msg.(*prepareRequest); ok { + pr.SetPrevHash(s.dbft.PrevHash) + pr.SetVersion(s.dbft.Version) + } + cp.SetPayload(msg) + + cp.Extensible.ValidBlockStart = 0 + cp.Extensible.ValidBlockEnd = c.BlockIndex + cp.Extensible.Sender = c.Validators[c.MyIndex].(*publicKey).GetScriptHash() + + return cp } func (s *service) newPrepareRequest() payload.PrepareRequest { @@ -257,7 +279,7 @@ events: s.dbft.OnTimeout(hv) case msg := <-s.messages: fields := []zap.Field{ - zap.Uint8("from", msg.validatorIndex), + zap.Uint8("from", msg.message.ValidatorIndex), zap.Stringer("type", msg.Type()), } @@ -312,14 +334,13 @@ func (s *service) handleChainBlock(b *coreb.Block) { func (s *service) validatePayload(p *Payload) bool { validators := s.getValidators() - if int(p.validatorIndex) >= len(validators) { + if int(p.message.ValidatorIndex) >= len(validators) { return false } - pub := validators[p.validatorIndex] + pub := validators[p.message.ValidatorIndex] h := pub.(*publicKey).GetScriptHash() - - return s.Chain.VerifyWitness(h, p, &p.Witness, payloadGasLimit) == nil + return p.Sender == h } func (s *service) getKeyPair(pubs []crypto.PublicKey) (int, crypto.PrivateKey, crypto.PublicKey) { @@ -346,14 +367,27 @@ func (s *service) getKeyPair(pubs []crypto.PublicKey) (int, crypto.PrivateKey, c return -1, nil, nil } +func (s *service) payloadFromExtensible(ep *npayload.Extensible) *Payload { + return &Payload{ + Extensible: *ep, + message: message{ + stateRootEnabled: s.stateRootEnabled, + }, + } +} + // OnPayload handles Payload receive. -func (s *service) OnPayload(cp *Payload) { +func (s *service) OnPayload(cp *npayload.Extensible) { log := s.log.With(zap.Stringer("hash", cp.Hash())) if s.cache.Has(cp.Hash()) { log.Debug("payload is already in cache") return - } else if !s.validatePayload(cp) { - log.Debug("can't validate payload") + } + + p := s.payloadFromExtensible(cp) + p.decodeData() + if !s.validatePayload(p) { + log.Info("can't validate payload") return } @@ -366,14 +400,14 @@ func (s *service) OnPayload(cp *Payload) { } // decode payload data into message - if cp.message.payload == nil { - if err := cp.decodeData(); err != nil { - log.Debug("can't decode payload data") + if p.message.payload == nil { + if err := p.decodeData(); err != nil { + log.Info("can't decode payload data") return } } - s.messages <- *cp + s.messages <- *p } func (s *service) OnTransaction(tx *transaction.Transaction) { @@ -383,13 +417,13 @@ func (s *service) OnTransaction(tx *transaction.Transaction) { } // GetPayload returns payload stored in cache. -func (s *service) GetPayload(h util.Uint256) *Payload { +func (s *service) GetPayload(h util.Uint256) *npayload.Extensible { p := s.cache.Get(h) if p == nil { - return (*Payload)(nil) + return (*npayload.Extensible)(nil) } - cp := *p.(*Payload) + cp := *p.(*npayload.Extensible) return &cp } @@ -399,8 +433,9 @@ func (s *service) broadcast(p payload.ConsensusPayload) { s.log.Warn("can't sign consensus payload", zap.Error(err)) } - s.cache.Add(p) - s.Config.Broadcast(p.(*Payload)) + ep := &p.(*Payload).Extensible + s.cache.Add(ep) + s.Config.Broadcast(ep) } func (s *service) getTx(h util.Uint256) block.Transaction { @@ -479,14 +514,26 @@ func (s *service) verifyBlock(b block.Block) bool { return true } +var ( + errInvalidPrevHash = errors.New("invalid PrevHash") + errInvalidVersion = errors.New("invalid Version") + errInvalidStateRoot = errors.New("state root mismatch") +) + func (s *service) verifyRequest(p payload.ConsensusPayload) error { req := p.GetPrepareRequest().(*prepareRequest) + if req.prevHash != s.dbft.PrevHash { + return errInvalidPrevHash + } + if req.version != s.dbft.Version { + return errInvalidVersion + } if s.stateRootEnabled { sr, err := s.Chain.GetStateRoot(s.dbft.BlockIndex - 1) if err != nil { return err } else if sr.Root != req.stateRoot { - return fmt.Errorf("state root mismatch: %s != %s", sr.Root, req.stateRoot) + return fmt.Errorf("%w: %s != %s", errInvalidStateRoot, sr.Root, req.stateRoot) } } // Save lastProposal for getVerified(). diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go index 39f74e224..3f8737dc0 100644 --- a/pkg/consensus/consensus_test.go +++ b/pkg/consensus/consensus_test.go @@ -1,12 +1,14 @@ package consensus import ( + "errors" "testing" "time" "github.com/nspcc-dev/dbft/block" "github.com/nspcc-dev/dbft/payload" "github.com/nspcc-dev/dbft/timer" + "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -19,6 +21,7 @@ import ( "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" + npayload "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" @@ -180,11 +183,10 @@ func TestService_GetVerified(t *testing.T) { // Everyone sends a message. for i := 0; i < 4; i++ { p := new(Payload) - p.message = &message{} // One PrepareRequest and three ChangeViews. if i == 1 { p.SetType(payload.PrepareRequestType) - p.SetPayload(&prepareRequest{transactionHashes: hashes}) + p.SetPayload(&prepareRequest{prevHash: srv.Chain.CurrentBlockHash(), transactionHashes: hashes}) } else { p.SetType(payload.ChangeViewType) p.SetPayload(&changeView{newViewNumber: 1, timestamp: uint64(time.Now().UnixNano() / nsInMs)}) @@ -224,8 +226,7 @@ func TestService_ValidatePayload(t *testing.T) { srv := newTestService(t) priv, _ := getTestValidator(1) p := new(Payload) - p.message = &message{} - + p.Sender = priv.GetScriptHash() p.SetPayload(&prepareRequest{}) t.Run("invalid validator index", func(t *testing.T) { @@ -243,8 +244,16 @@ func TestService_ValidatePayload(t *testing.T) { require.False(t, srv.validatePayload(p)) }) + t.Run("invalid sender", func(t *testing.T) { + p.SetValidatorIndex(1) + p.Sender = util.Uint160{} + require.NoError(t, p.Sign(priv)) + require.False(t, srv.validatePayload(p)) + }) + t.Run("normal case", func(t *testing.T) { p.SetValidatorIndex(1) + p.Sender = priv.GetScriptHash() require.NoError(t, p.Sign(priv)) require.True(t, srv.validatePayload(p)) }) @@ -295,22 +304,35 @@ func TestService_PrepareRequest(t *testing.T) { priv, _ := getTestValidator(1) p := new(Payload) - p.message = &message{} p.SetValidatorIndex(1) - p.SetPayload(&prepareRequest{}) - require.NoError(t, p.Sign(priv)) - require.Error(t, srv.verifyRequest(p), "invalid stateroot setting") + prevHash := srv.Chain.CurrentBlockHash() - p.SetPayload(&prepareRequest{stateRootEnabled: true}) - require.NoError(t, p.Sign(priv)) - require.Error(t, srv.verifyRequest(p), "invalid state root") + checkRequest := func(t *testing.T, expectedErr error, req *prepareRequest) { + p.SetPayload(req) + require.NoError(t, p.Sign(priv)) + err := srv.verifyRequest(p) + if expectedErr == nil { + require.NoError(t, err) + return + } + require.True(t, errors.Is(err, expectedErr), "got: %v", err) + } + + checkRequest(t, errInvalidVersion, &prepareRequest{version: 0xFF, prevHash: prevHash}) + checkRequest(t, errInvalidPrevHash, &prepareRequest{prevHash: random.Uint256()}) + checkRequest(t, errInvalidStateRoot, &prepareRequest{ + stateRootEnabled: true, + prevHash: prevHash, + }) sr, err := srv.Chain.GetStateRoot(srv.dbft.BlockIndex - 1) require.NoError(t, err) - p.SetPayload(&prepareRequest{stateRootEnabled: true, stateRoot: sr.Root}) - require.NoError(t, p.Sign(priv)) - require.NoError(t, srv.verifyRequest(p)) + checkRequest(t, nil, &prepareRequest{ + stateRootEnabled: true, + prevHash: prevHash, + stateRoot: sr.Root, + }) } func TestService_OnPayload(t *testing.T) { @@ -322,22 +344,26 @@ func TestService_OnPayload(t *testing.T) { priv, _ := getTestValidator(1) p := new(Payload) - p.message = &message{} p.SetValidatorIndex(1) p.SetPayload(&prepareRequest{}) + p.encodeData() - // payload is not signed - srv.OnPayload(p) + // sender is invalid + srv.OnPayload(&p.Extensible) shouldNotReceive(t, srv.messages) require.Nil(t, srv.GetPayload(p.Hash())) + p = new(Payload) + p.SetValidatorIndex(1) + p.Sender = priv.GetScriptHash() + p.SetPayload(&prepareRequest{}) require.NoError(t, p.Sign(priv)) - srv.OnPayload(p) + srv.OnPayload(&p.Extensible) shouldReceive(t, srv.messages) - require.Equal(t, p, srv.GetPayload(p.Hash())) + require.Equal(t, &p.Extensible, srv.GetPayload(p.Hash())) // payload has already been received - srv.OnPayload(p) + srv.OnPayload(&p.Extensible) shouldNotReceive(t, srv.messages) srv.Chain.Close() } @@ -453,7 +479,7 @@ func newTestService(t *testing.T) *service { func newTestServiceWithChain(t *testing.T, bc *core.Blockchain) *service { srv, err := NewService(Config{ Logger: zaptest.NewLogger(t), - Broadcast: func(*Payload) {}, + Broadcast: func(*npayload.Extensible) {}, Chain: bc, RequestTx: func(...util.Uint256) {}, TimePerBlock: time.Duration(bc.GetConfig().SecondsPerBlock) * time.Second, diff --git a/pkg/consensus/payload.go b/pkg/consensus/payload.go index ce563e4eb..ffcaf24fc 100644 --- a/pkg/consensus/payload.go +++ b/pkg/consensus/payload.go @@ -1,14 +1,11 @@ package consensus import ( - "errors" "fmt" "github.com/nspcc-dev/dbft/payload" - "github.com/nspcc-dev/neo-go/pkg/config/netmode" - "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" + npayload "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" ) @@ -17,8 +14,10 @@ type ( messageType byte message struct { - Type messageType - ViewNumber byte + Type messageType + BlockIndex uint32 + ValidatorIndex byte + ViewNumber byte payload io.Serializable // stateRootEnabled specifies if state root is exchanged during consensus. @@ -27,20 +26,8 @@ type ( // Payload is a type for consensus-related messages. Payload struct { - *message - - network netmode.Magic - data []byte - version uint32 - validatorIndex uint8 - prevHash util.Uint256 - height uint32 - - Witness transaction.Witness - - hash util.Uint256 - signedHash util.Uint256 - signedpart []byte + npayload.Extensible + message } ) @@ -111,99 +98,36 @@ func (p Payload) GetRecoveryMessage() payload.RecoveryMessage { return p.payload.(payload.RecoveryMessage) } -// MarshalUnsigned implements payload.ConsensusPayload interface. -func (p *Payload) MarshalUnsigned() []byte { - if p.signedpart == nil { - w := io.NewBufBinWriter() - p.encodeHashData(w.BinWriter) - p.signedpart = w.Bytes() - } - return p.signedpart -} - -// UnmarshalUnsigned implements payload.ConsensusPayload interface. -func (p *Payload) UnmarshalUnsigned(data []byte) error { - r := io.NewBinReaderFromBuf(data) - p.network = netmode.Magic(r.ReadU32LE()) - p.DecodeBinaryUnsigned(r) - - return r.Err -} - -// Version implements payload.ConsensusPayload interface. -func (p Payload) Version() uint32 { - return p.version -} - -// SetVersion implements payload.ConsensusPayload interface. -func (p *Payload) SetVersion(v uint32) { - p.version = v -} - // ValidatorIndex implements payload.ConsensusPayload interface. func (p Payload) ValidatorIndex() uint16 { - return uint16(p.validatorIndex) + return uint16(p.message.ValidatorIndex) } // SetValidatorIndex implements payload.ConsensusPayload interface. func (p *Payload) SetValidatorIndex(i uint16) { - p.validatorIndex = uint8(i) -} - -// PrevHash implements payload.ConsensusPayload interface. -func (p Payload) PrevHash() util.Uint256 { - return p.prevHash -} - -// SetPrevHash implements payload.ConsensusPayload interface. -func (p *Payload) SetPrevHash(h util.Uint256) { - p.prevHash = h + p.message.ValidatorIndex = byte(i) } // Height implements payload.ConsensusPayload interface. func (p Payload) Height() uint32 { - return p.height + return p.message.BlockIndex } // SetHeight implements payload.ConsensusPayload interface. func (p *Payload) SetHeight(h uint32) { - p.height = h -} - -// EncodeBinaryUnsigned writes payload to w excluding signature. -func (p *Payload) EncodeBinaryUnsigned(w *io.BinWriter) { - w.WriteU32LE(p.version) - w.WriteBytes(p.prevHash[:]) - w.WriteU32LE(p.height) - w.WriteB(p.validatorIndex) - - if p.data == nil { - ww := io.NewBufBinWriter() - p.message.EncodeBinary(ww.BinWriter) - p.data = ww.Bytes() - } - w.WriteVarBytes(p.data) + p.message.BlockIndex = h } // EncodeBinary implements io.Serializable interface. func (p *Payload) EncodeBinary(w *io.BinWriter) { - if p.signedpart == nil { - _ = p.MarshalUnsigned() - } - w.WriteBytes(p.signedpart[4:]) - - w.WriteB(1) - p.Witness.EncodeBinary(w) -} - -func (p *Payload) encodeHashData(w *io.BinWriter) { - w.WriteU32LE(uint32(p.network)) - p.EncodeBinaryUnsigned(w) + p.encodeData() + p.Extensible.EncodeBinary(w) } // Sign signs payload using the private key. // It also sets corresponding verification and invocation scripts. func (p *Payload) Sign(key *privateKey) error { + p.encodeData() sig := key.SignHash(p.GetSignedHash()) buf := io.NewBufBinWriter() @@ -216,78 +140,39 @@ func (p *Payload) Sign(key *privateKey) error { // GetSignedPart implements crypto.Verifiable interface. func (p *Payload) GetSignedPart() []byte { - return p.MarshalUnsigned() -} - -// DecodeBinaryUnsigned reads payload from w excluding signature. -func (p *Payload) DecodeBinaryUnsigned(r *io.BinReader) { - p.version = r.ReadU32LE() - r.ReadBytes(p.prevHash[:]) - p.height = r.ReadU32LE() - p.validatorIndex = r.ReadB() - - p.data = r.ReadVarBytes() - if r.Err != nil { - return + if p.Extensible.Data == nil { + p.encodeData() } + return p.Extensible.GetSignedPart() } // GetSignedHash returns a hash of the payload used to verify it. func (p *Payload) GetSignedHash() util.Uint256 { - if p.signedHash.Equals(util.Uint256{}) { - if p.createHash() != nil { - panic("failed to compute hash!") - } + if p.Extensible.Data == nil { + p.encodeData() } - return p.signedHash + return p.Extensible.GetSignedHash() } // Hash implements payload.ConsensusPayload interface. func (p *Payload) Hash() util.Uint256 { - if p.hash.Equals(util.Uint256{}) { - if p.createHash() != nil { - panic("failed to compute hash!") - } + if p.Extensible.Data == nil { + p.encodeData() } - return p.hash -} - -// createHash creates hashes of the payload. -func (p *Payload) createHash() error { - b := p.GetSignedPart() - if b == nil { - return errors.New("failed to serialize hashable data") - } - p.updateHashes(b) - return nil -} - -// updateHashes updates Payload's hashes based on the given buffer which should -// be a signable data slice. -func (p *Payload) updateHashes(b []byte) { - p.signedHash = hash.Sha256(b) - p.hash = hash.Sha256(p.signedHash.BytesBE()) + return p.Extensible.Hash() } // DecodeBinary implements io.Serializable interface. func (p *Payload) DecodeBinary(r *io.BinReader) { - p.DecodeBinaryUnsigned(r) - if r.Err != nil { - return - } - - var b = r.ReadB() - if b != 1 { - r.Err = errors.New("invalid format") - return - } - - p.Witness.DecodeBinary(r) + p.Extensible.DecodeBinary(r) + p.decodeData() } // EncodeBinary implements io.Serializable interface. func (m *message) EncodeBinary(w *io.BinWriter) { - w.WriteBytes([]byte{byte(m.Type)}) + w.WriteB(byte(m.Type)) + w.WriteU32LE(m.BlockIndex) + w.WriteB(m.ValidatorIndex) w.WriteB(m.ViewNumber) m.payload.EncodeBinary(w) } @@ -295,6 +180,8 @@ func (m *message) EncodeBinary(w *io.BinWriter) { // DecodeBinary implements io.Serializable interface. func (m *message) DecodeBinary(r *io.BinReader) { m.Type = messageType(r.ReadB()) + m.BlockIndex = r.ReadU32LE() + m.ValidatorIndex = r.ReadB() m.ViewNumber = r.ReadB() switch m.Type { @@ -348,14 +235,22 @@ func (t messageType) String() string { } } +func (p *Payload) encodeData() { + if p.Extensible.Data == nil { + p.Extensible.ValidBlockStart = 0 + p.Extensible.ValidBlockEnd = p.BlockIndex + bw := io.NewBufBinWriter() + p.message.EncodeBinary(bw.BinWriter) + p.Extensible.Data = bw.Bytes() + } +} + // decode data of payload into it's message func (p *Payload) decodeData() error { - m := p.message - br := io.NewBinReaderFromBuf(p.data) - m.DecodeBinary(br) + br := io.NewBinReaderFromBuf(p.Extensible.Data) + p.message.DecodeBinary(br) if br.Err != nil { return fmt.Errorf("can't decode message: %w", br.Err) } - p.message = m return nil } diff --git a/pkg/consensus/payload_test.go b/pkg/consensus/payload_test.go index 92e0b4df0..4ce63e3e3 100644 --- a/pkg/consensus/payload_test.go +++ b/pkg/consensus/payload_test.go @@ -1,18 +1,16 @@ package consensus import ( - "encoding/hex" - gio "io" "math/rand" "testing" "github.com/nspcc-dev/dbft/payload" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/testserdes" - "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/io" + npayload "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/stretchr/testify/assert" @@ -30,13 +28,12 @@ var messageTypes = []messageType{ func TestConsensusPayload_Setters(t *testing.T) { var p Payload - p.message = &message{} - p.SetVersion(1) - assert.EqualValues(t, 1, p.Version()) + //p.SetVersion(1) + //assert.EqualValues(t, 1, p.Version()) - p.SetPrevHash(util.Uint256{1, 2, 3}) - assert.Equal(t, util.Uint256{1, 2, 3}, p.PrevHash()) + //p.SetPrevHash(util.Uint256{1, 2, 3}) + //assert.Equal(t, util.Uint256{1, 2, 3}, p.PrevHash()) p.SetValidatorIndex(4) assert.EqualValues(t, 4, p.ValidatorIndex()) @@ -76,22 +73,22 @@ func TestConsensusPayload_Setters(t *testing.T) { require.Equal(t, pl, p.GetRecoveryMessage()) } -func TestConsensusPayload_Verify(t *testing.T) { - // signed payload from mixed privnet (Go + 3C# nodes) - dataHex := "000000004c02d52305a6a8981bd1598f0c3076d6de15a44a60ca692e189cd8a7249f175c0800000003222100f24b9147a21e09562c68abdec56d3c5fc09936592933aea5692800b75edbab2301420c40b2b8080ab02b703bc4e64407a6f31bb7ae4c9b1b1c8477668afa752eba6148e03b3ffc7e06285c09bdce4582188466209f876c38f9921a88b545393543ab201a290c2103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee6990b4195440d78" - data, err := hex.DecodeString(dataHex) - require.NoError(t, err) - - h, err := util.Uint160DecodeStringLE("a8826043c40abacfac1d9acc6b92a4458308ca18") - require.NoError(t, err) - - p := NewPayload(netmode.PrivNet, false) - require.NoError(t, testserdes.DecodeBinary(data, p)) - require.NoError(t, p.decodeData()) - bc := newTestChain(t, false) - defer bc.Close() - require.NoError(t, bc.VerifyWitness(h, p, &p.Witness, payloadGasLimit)) -} +//func TestConsensusPayload_Verify(t *testing.T) { +// // signed payload from mixed privnet (Go + 3C# nodes) +// dataHex := "000000004c02d52305a6a8981bd1598f0c3076d6de15a44a60ca692e189cd8a7249f175c0800000003222100f24b9147a21e09562c68abdec56d3c5fc09936592933aea5692800b75edbab2301420c40b2b8080ab02b703bc4e64407a6f31bb7ae4c9b1b1c8477668afa752eba6148e03b3ffc7e06285c09bdce4582188466209f876c38f9921a88b545393543ab201a290c2103d90c07df63e690ce77912e10ab51acc944b66860237b608c4f8f8309e71ee6990b4195440d78" +// data, err := hex.DecodeString(dataHex) +// require.NoError(t, err) +// +// h, err := util.Uint160DecodeStringLE("a8826043c40abacfac1d9acc6b92a4458308ca18") +// require.NoError(t, err) +// +// p := NewPayload(netmode.PrivNet, false) +// require.NoError(t, testserdes.DecodeBinary(data, p)) +// require.NoError(t, p.decodeData()) +// bc := newTestChain(t, false) +// defer bc.Close() +// require.NoError(t, bc.VerifyWitness(h, p, &p.Witness, payloadGasLimit)) +//} func TestConsensusPayload_Serializable(t *testing.T) { for _, mt := range messageTypes { @@ -99,85 +96,70 @@ func TestConsensusPayload_Serializable(t *testing.T) { actual := new(Payload) data, err := testserdes.EncodeBinary(p) require.NoError(t, err) - require.NoError(t, testserdes.DecodeBinary(data, actual)) - // message is nil after decoding as we didn't yet call decodeData - require.Nil(t, actual.message) - actual.message = new(message) - // message should now be decoded from actual.data byte array - actual.message = new(message) + require.NoError(t, testserdes.DecodeBinary(data, &actual.Extensible)) assert.NoError(t, actual.decodeData()) - assert.NotNil(t, actual.MarshalUnsigned()) require.Equal(t, p, actual) - - data = p.MarshalUnsigned() - pu := NewPayload(netmode.Magic(rand.Uint32()), false) - require.NoError(t, pu.UnmarshalUnsigned(data)) - assert.NoError(t, pu.decodeData()) - _ = pu.MarshalUnsigned() - - p.Witness = transaction.Witness{} - require.Equal(t, p, pu) } } -func TestConsensusPayload_DecodeBinaryInvalid(t *testing.T) { - // PrepareResponse ConsensusPayload consists of: - // 41-byte common prefix - // 1-byte varint length of the payload (34), - // - 1-byte view number - // - 1-byte message type (PrepareResponse) - // - 32-byte preparation hash - // 1-byte delimiter (1) - // 2-byte for empty invocation and verification scripts - const ( - lenIndex = 41 - typeIndex = lenIndex + 1 - delimeterIndex = typeIndex + 34 - ) - - buf := make([]byte, delimeterIndex+1+2) - - expected := &Payload{ - message: &message{ - Type: prepareResponseType, - payload: &prepareResponse{}, - }, - Witness: transaction.Witness{ - InvocationScript: []byte{}, - VerificationScript: []byte{}, - }, - } - // fill `data` for next check - _ = expected.Hash() - - // valid payload - buf[delimeterIndex] = 1 - buf[lenIndex] = 34 - buf[typeIndex] = byte(prepareResponseType) - p := &Payload{message: new(message)} - require.NoError(t, testserdes.DecodeBinary(buf, p)) - // decode `data` into `message` - _ = p.Hash() - assert.NoError(t, p.decodeData()) - require.Equal(t, expected, p) - - // invalid type - buf[typeIndex] = 0xFF - actual := &Payload{message: new(message)} - require.NoError(t, testserdes.DecodeBinary(buf, actual)) - require.Error(t, actual.decodeData()) - - // invalid format - buf[delimeterIndex] = 0 - buf[typeIndex] = byte(prepareResponseType) - require.Error(t, testserdes.DecodeBinary(buf, new(Payload))) - - // invalid message length - buf[delimeterIndex] = 1 - buf[lenIndex] = 0xFF - buf[typeIndex] = byte(prepareResponseType) - require.Error(t, testserdes.DecodeBinary(buf, new(Payload))) -} +//func TestConsensusPayload_DecodeBinaryInvalid(t *testing.T) { +// // PrepareResponse ConsensusPayload consists of: +// // 41-byte common prefix +// // 1-byte varint length of the payload (34), +// // - 1-byte view number +// // - 1-byte message type (PrepareResponse) +// // - 32-byte preparation hash +// // 1-byte delimiter (1) +// // 2-byte for empty invocation and verification scripts +// const ( +// lenIndex = 41 +// typeIndex = lenIndex + 1 +// delimeterIndex = typeIndex + 34 +// ) +// +// buf := make([]byte, delimeterIndex+1+2) +// +// expected := &Payload{ +// message: &message{ +// Type: prepareResponseType, +// payload: &prepareResponse{}, +// }, +// Extensible: transaction.Witness{ +// InvocationScript: []byte{}, +// VerificationScript: []byte{}, +// }, +// } +// // fill `data` for next check +// _ = expected.Hash() +// +// // valid payload +// buf[delimeterIndex] = 1 +// buf[lenIndex] = 34 +// buf[typeIndex] = byte(prepareResponseType) +// p := &Payload{message: new(message)} +// require.NoError(t, testserdes.DecodeBinary(buf, p)) +// // decode `data` into `message` +// _ = p.Hash() +// assert.NoError(t, p.decodeData()) +// require.Equal(t, expected, p) +// +// // invalid type +// buf[typeIndex] = 0xFF +// actual := &Payload{message: new(message)} +// require.NoError(t, testserdes.DecodeBinary(buf, actual)) +// require.Error(t, actual.decodeData()) +// +// // invalid format +// buf[delimeterIndex] = 0 +// buf[typeIndex] = byte(prepareResponseType) +// require.Error(t, testserdes.DecodeBinary(buf, new(Payload))) +// +// // invalid message length +// buf[delimeterIndex] = 1 +// buf[lenIndex] = 0xFF +// buf[typeIndex] = byte(prepareResponseType) +// require.Error(t, testserdes.DecodeBinary(buf, new(Payload))) +//} func TestCommit_Serializable(t *testing.T) { c := randomMessage(t, commitType) @@ -206,18 +188,18 @@ func TestRecoveryMessage_Serializable(t *testing.T) { func randomPayload(t *testing.T, mt messageType) *Payload { p := &Payload{ - message: &message{ - Type: mt, - ViewNumber: byte(rand.Uint32()), - payload: randomMessage(t, mt), + message: message{ + Type: mt, + ValidatorIndex: byte(rand.Uint32()), + BlockIndex: rand.Uint32(), + ViewNumber: byte(rand.Uint32()), + payload: randomMessage(t, mt), }, - version: 1, - validatorIndex: 13, - height: rand.Uint32(), - prevHash: random.Uint256(), - Witness: transaction.Witness{ - InvocationScript: random.Bytes(3), - VerificationScript: []byte{byte(opcode.PUSH0)}, + Extensible: npayload.Extensible{ + Witness: transaction.Witness{ + InvocationScript: random.Bytes(3), + VerificationScript: []byte{byte(opcode.PUSH0)}, + }, }, } @@ -334,19 +316,19 @@ func TestMessageType_String(t *testing.T) { require.Equal(t, "UNKNOWN(0xff)", messageType(0xff).String()) } -func TestPayload_DecodeFromPrivnet(t *testing.T) { - hexDump := "000000004c02d52305a6a8981bd1598f0c3076d6de15a44a60ca692e189cd8a7249f175c08000000004230000368c5c5401d40eef6b8a9899d2041d29fd2e6300980fdcaa6660c10b85965f57852193cdb6f0d1e9f91dc510dff6df3a004b569fe2ad456d07007f6ccd55b1d01420c40e760250b821a4dcfc4b8727ecc409a758ab4bd3b288557fd3c3d76e083fe7c625b4ed25e763ad96c4eb0abc322600d82651fd32f8866fca1403fa04d3acc4675290c2102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e0b4195440d78" - data, err := hex.DecodeString(hexDump) - require.NoError(t, err) - - buf := io.NewBinReaderFromBuf(data) - p := NewPayload(netmode.PrivNet, false) - p.DecodeBinary(buf) - require.NoError(t, buf.Err) - require.NoError(t, p.decodeData()) - require.Equal(t, payload.CommitType, p.Type()) - require.Equal(t, uint32(8), p.Height()) - - buf.ReadB() - require.Equal(t, gio.EOF, buf.Err) -} +//func TestPayload_DecodeFromPrivnet(t *testing.T) { +// hexDump := "000000004c02d52305a6a8981bd1598f0c3076d6de15a44a60ca692e189cd8a7249f175c08000000004230000368c5c5401d40eef6b8a9899d2041d29fd2e6300980fdcaa6660c10b85965f57852193cdb6f0d1e9f91dc510dff6df3a004b569fe2ad456d07007f6ccd55b1d01420c40e760250b821a4dcfc4b8727ecc409a758ab4bd3b288557fd3c3d76e083fe7c625b4ed25e763ad96c4eb0abc322600d82651fd32f8866fca1403fa04d3acc4675290c2102103a7f7dd016558597f7960d27c516a4394fd968b9e65155eb4b013e4040406e0b4195440d78" +// data, err := hex.DecodeString(hexDump) +// require.NoError(t, err) +// +// buf := io.NewBinReaderFromBuf(data) +// p := NewPayload(netmode.PrivNet, false) +// p.DecodeBinary(buf) +// require.NoError(t, buf.Err) +// require.NoError(t, p.decodeData()) +// require.Equal(t, payload.CommitType, p.Type()) +// require.Equal(t, uint32(8), p.Height()) +// +// buf.ReadB() +// require.Equal(t, gio.EOF, buf.Err) +//} diff --git a/pkg/consensus/prepare_request.go b/pkg/consensus/prepare_request.go index 9099740fe..4cbc61f04 100644 --- a/pkg/consensus/prepare_request.go +++ b/pkg/consensus/prepare_request.go @@ -9,6 +9,8 @@ import ( // prepareRequest represents dBFT prepareRequest message. type prepareRequest struct { + version uint32 + prevHash util.Uint256 timestamp uint64 nonce uint64 transactionHashes []util.Uint256 @@ -20,6 +22,8 @@ var _ payload.PrepareRequest = (*prepareRequest)(nil) // EncodeBinary implements io.Serializable interface. func (p *prepareRequest) EncodeBinary(w *io.BinWriter) { + w.WriteU32LE(p.version) + w.WriteBytes(p.prevHash[:]) w.WriteU64LE(p.timestamp) w.WriteU64LE(p.nonce) w.WriteArray(p.transactionHashes) @@ -30,6 +34,8 @@ func (p *prepareRequest) EncodeBinary(w *io.BinWriter) { // DecodeBinary implements io.Serializable interface. func (p *prepareRequest) DecodeBinary(r *io.BinReader) { + p.version = r.ReadU32LE() + r.ReadBytes(p.prevHash[:]) p.timestamp = r.ReadU64LE() p.nonce = r.ReadU64LE() r.ReadArray(&p.transactionHashes, block.MaxTransactionsPerBlock) @@ -38,6 +44,26 @@ func (p *prepareRequest) DecodeBinary(r *io.BinReader) { } } +// Version implements payload.PrepareRequest interface. +func (p prepareRequest) Version() uint32 { + return p.version +} + +// SetVersion implements payload.PrepareRequest interface. +func (p *prepareRequest) SetVersion(v uint32) { + p.version = v +} + +// PrevHash implements payload.PrepareRequest interface. +func (p prepareRequest) PrevHash() util.Uint256 { + return p.prevHash +} + +// SetPrevHash implements payload.PrepareRequest interface. +func (p *prepareRequest) SetPrevHash(h util.Uint256) { + p.prevHash = h +} + // Timestamp implements payload.PrepareRequest interface. func (p *prepareRequest) Timestamp() uint64 { return p.timestamp * nsInMs } diff --git a/pkg/consensus/recovery_message.go b/pkg/consensus/recovery_message.go index c094c7f3c..f24376cb8 100644 --- a/pkg/consensus/recovery_message.go +++ b/pkg/consensus/recovery_message.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/dbft/crypto" "github.com/nspcc-dev/dbft/payload" "github.com/nspcc-dev/neo-go/pkg/io" + npayload "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -202,6 +203,7 @@ func (m *recoveryMessage) GetPrepareRequest(p payload.ConsensusPayload, validato req := fromPayload(prepareRequestType, p.(*Payload), m.prepareRequest.payload) req.SetValidatorIndex(primary) + req.Sender = validators[primary].(*publicKey).GetScriptHash() req.Witness.InvocationScript = compact.InvocationScript req.Witness.VerificationScript = getVerificationScript(uint8(primary), validators) @@ -221,6 +223,7 @@ func (m *recoveryMessage) GetPrepareResponses(p payload.ConsensusPayload, valida preparationHash: *m.preparationHash, }) r.SetValidatorIndex(uint16(resp.ValidatorIndex)) + r.Sender = validators[resp.ValidatorIndex].(*publicKey).GetScriptHash() r.Witness.InvocationScript = resp.InvocationScript r.Witness.VerificationScript = getVerificationScript(resp.ValidatorIndex, validators) @@ -241,6 +244,7 @@ func (m *recoveryMessage) GetChangeViews(p payload.ConsensusPayload, validators }) c.message.ViewNumber = cv.OriginalViewNumber c.SetValidatorIndex(uint16(cv.ValidatorIndex)) + c.Sender = validators[cv.ValidatorIndex].(*publicKey).GetScriptHash() c.Witness.InvocationScript = cv.InvocationScript c.Witness.VerificationScript = getVerificationScript(cv.ValidatorIndex, validators) @@ -257,6 +261,7 @@ func (m *recoveryMessage) GetCommits(p payload.ConsensusPayload, validators []cr for i, c := range m.commitPayloads { cc := fromPayload(commitType, p.(*Payload), &commit{signature: c.Signature}) cc.SetValidatorIndex(uint16(c.ValidatorIndex)) + cc.Sender = validators[c.ValidatorIndex].(*publicKey).GetScriptHash() cc.Witness.InvocationScript = c.InvocationScript cc.Witness.VerificationScript = getVerificationScript(c.ValidatorIndex, validators) @@ -291,15 +296,17 @@ func getVerificationScript(i uint8, validators []crypto.PublicKey) []byte { func fromPayload(t messageType, recovery *Payload, p io.Serializable) *Payload { return &Payload{ - network: recovery.network, - message: &message{ + Extensible: npayload.Extensible{ + Category: Category, + Network: recovery.Network, + ValidBlockEnd: recovery.BlockIndex, + }, + message: message{ Type: t, + BlockIndex: recovery.BlockIndex, ViewNumber: recovery.message.ViewNumber, payload: p, stateRootEnabled: recovery.stateRootEnabled, }, - version: recovery.Version(), - prevHash: recovery.PrevHash(), - height: recovery.Height(), } } diff --git a/pkg/consensus/recovery_message_test.go b/pkg/consensus/recovery_message_test.go index 0759ae7fa..d0ae10fef 100644 --- a/pkg/consensus/recovery_message_test.go +++ b/pkg/consensus/recovery_message_test.go @@ -30,9 +30,12 @@ func testRecoveryMessageSetters(t *testing.T, enableStateRoot bool) { privs[i], pubs[i] = getTestValidator(i) } + const msgHeight = 10 + r := &recoveryMessage{stateRootEnabled: enableStateRoot} p := NewPayload(netmode.UnitTestNet, enableStateRoot) p.SetType(payload.RecoveryMessageType) + p.SetHeight(msgHeight) p.SetPayload(r) // sign payload to have verification script require.NoError(t, p.Sign(privs[0])) @@ -45,17 +48,21 @@ func testRecoveryMessageSetters(t *testing.T, enableStateRoot bool) { } p1 := NewPayload(netmode.UnitTestNet, enableStateRoot) p1.SetType(payload.PrepareRequestType) + p1.SetHeight(msgHeight) p1.SetPayload(req) p1.SetValidatorIndex(0) + p1.Sender = privs[0].GetScriptHash() require.NoError(t, p1.Sign(privs[0])) t.Run("prepare response is added", func(t *testing.T) { p2 := NewPayload(netmode.UnitTestNet, enableStateRoot) p2.SetType(payload.PrepareResponseType) + p2.SetHeight(msgHeight) p2.SetPayload(&prepareResponse{ preparationHash: p1.Hash(), }) p2.SetValidatorIndex(1) + p2.Sender = privs[1].GetScriptHash() require.NoError(t, p2.Sign(privs[1])) r.AddPayload(p2) @@ -88,11 +95,13 @@ func testRecoveryMessageSetters(t *testing.T, enableStateRoot bool) { t.Run("change view is added", func(t *testing.T) { p3 := NewPayload(netmode.UnitTestNet, enableStateRoot) p3.SetType(payload.ChangeViewType) + p3.SetHeight(msgHeight) p3.SetPayload(&changeView{ newViewNumber: 1, timestamp: 12345, }) p3.SetValidatorIndex(3) + p3.Sender = privs[3].GetScriptHash() require.NoError(t, p3.Sign(privs[3])) r.AddPayload(p3) @@ -110,8 +119,10 @@ func testRecoveryMessageSetters(t *testing.T, enableStateRoot bool) { t.Run("commit is added", func(t *testing.T) { p4 := NewPayload(netmode.UnitTestNet, enableStateRoot) p4.SetType(payload.CommitType) + p4.SetHeight(msgHeight) p4.SetPayload(randomMessage(t, commitType)) p4.SetValidatorIndex(3) + p4.Sender = privs[3].GetScriptHash() require.NoError(t, p4.Sign(privs[3])) r.AddPayload(p4) diff --git a/pkg/network/message.go b/pkg/network/message.go index c8bf606ae..9c64cc802 100644 --- a/pkg/network/message.go +++ b/pkg/network/message.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/nspcc-dev/neo-go/pkg/config/netmode" - "github.com/nspcc-dev/neo-go/pkg/consensus" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/io" @@ -74,7 +73,7 @@ const ( CMDNotFound CommandType = 0x2a CMDTX = CommandType(payload.TXType) CMDBlock = CommandType(payload.BlockType) - CMDConsensus = CommandType(payload.ConsensusType) + CMDExtensible = CommandType(payload.ExtensibleType) CMDP2PNotaryRequest = CommandType(payload.P2PNotaryRequestType) CMDReject CommandType = 0x2f @@ -147,8 +146,8 @@ func (m *Message) decodePayload() error { p = &payload.AddressList{} case CMDBlock: p = block.New(m.Network, m.StateRootInHeader) - case CMDConsensus: - p = consensus.NewPayload(m.Network, m.StateRootInHeader) + case CMDExtensible: + p = payload.NewExtensible(m.Network) case CMDP2PNotaryRequest: p = &payload.P2PNotaryRequest{Network: m.Network} case CMDGetBlocks: diff --git a/pkg/network/message_string.go b/pkg/network/message_string.go index 233c9084b..7da007079 100644 --- a/pkg/network/message_string.go +++ b/pkg/network/message_string.go @@ -24,7 +24,8 @@ func _() { _ = x[CMDNotFound-42] _ = x[CMDTX-43] _ = x[CMDBlock-44] - _ = x[CMDConsensus-45] + _ = x[CMDExtensible-46] + _ = x[CMDP2PNotaryRequest-80] _ = x[CMDReject-47] _ = x[CMDFilterLoad-48] _ = x[CMDFilterAdd-49] @@ -39,10 +40,11 @@ const ( _CommandType_name_2 = "CMDPingCMDPong" _CommandType_name_3 = "CMDGetHeadersCMDHeaders" _CommandType_name_4 = "CMDGetBlocksCMDMempool" - _CommandType_name_5 = "CMDInvCMDGetDataCMDGetBlockByIndexCMDNotFoundCMDTXCMDBlockCMDConsensus" - _CommandType_name_6 = "CMDRejectCMDFilterLoadCMDFilterAddCMDFilterClear" + _CommandType_name_5 = "CMDInvCMDGetDataCMDGetBlockByIndexCMDNotFoundCMDTXCMDBlock" + _CommandType_name_6 = "CMDExtensibleCMDRejectCMDFilterLoadCMDFilterAddCMDFilterClear" _CommandType_name_7 = "CMDMerkleBlock" _CommandType_name_8 = "CMDAlert" + _CommandType_name_9 = "CMDP2PNotaryRequest" ) var ( @@ -51,8 +53,8 @@ var ( _CommandType_index_2 = [...]uint8{0, 7, 14} _CommandType_index_3 = [...]uint8{0, 13, 23} _CommandType_index_4 = [...]uint8{0, 12, 22} - _CommandType_index_5 = [...]uint8{0, 6, 16, 34, 45, 50, 58, 70} - _CommandType_index_6 = [...]uint8{0, 9, 22, 34, 48} + _CommandType_index_5 = [...]uint8{0, 6, 16, 34, 45, 50, 58} + _CommandType_index_6 = [...]uint8{0, 13, 22, 35, 47, 61} ) func (i CommandType) String() string { @@ -71,16 +73,18 @@ func (i CommandType) String() string { case 36 <= i && i <= 37: i -= 36 return _CommandType_name_4[_CommandType_index_4[i]:_CommandType_index_4[i+1]] - case 39 <= i && i <= 45: + case 39 <= i && i <= 44: i -= 39 return _CommandType_name_5[_CommandType_index_5[i]:_CommandType_index_5[i+1]] - case 47 <= i && i <= 50: - i -= 47 + case 46 <= i && i <= 50: + i -= 46 return _CommandType_name_6[_CommandType_index_6[i]:_CommandType_index_6[i+1]] case i == 56: return _CommandType_name_7 case i == 64: return _CommandType_name_8 + case i == 80: + return _CommandType_name_9 default: return "CommandType(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/pkg/network/message_test.go b/pkg/network/message_test.go index df2010b88..697c7c7da 100644 --- a/pkg/network/message_test.go +++ b/pkg/network/message_test.go @@ -96,7 +96,7 @@ func TestEncodeDecodePing(t *testing.T) { } func TestEncodeDecodeInventory(t *testing.T) { - testEncodeDecode(t, CMDInv, payload.NewInventory(payload.ConsensusType, []util.Uint256{{1, 2, 3}})) + testEncodeDecode(t, CMDInv, payload.NewInventory(payload.ExtensibleType, []util.Uint256{{1, 2, 3}})) } func TestEncodeDecodeAddr(t *testing.T) { diff --git a/pkg/network/payload/extensible.go b/pkg/network/payload/extensible.go new file mode 100644 index 000000000..7df9ffd33 --- /dev/null +++ b/pkg/network/payload/extensible.go @@ -0,0 +1,126 @@ +package payload + +import ( + "errors" + "math" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "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" +) + +const ( + maxExtensibleCategorySize = 32 + maxExtensibleDataSize = math.MaxUint16 +) + +// Extensible represents payload containing arbitrary data. +type Extensible struct { + // Network represents network magic. + Network netmode.Magic + // Category is payload type. + Category string + // ValidBlockStart is starting height for payload to be valid. + ValidBlockStart uint32 + // ValidBlockEnd is height after which payload becomes invalid. + ValidBlockEnd uint32 + // Sender is payload sender or signer. + Sender util.Uint160 + // Data is custom payload data. + Data []byte + // Witness is payload witness. + Witness transaction.Witness + + hash util.Uint256 + signedHash util.Uint256 + signedpart []byte +} + +var errInvalidPadding = errors.New("invalid padding") + +// NewExtensible creates new extensible payload. +func NewExtensible(network netmode.Magic) *Extensible { + return &Extensible{Network: network} +} + +func (e *Extensible) encodeBinaryUnsigned(w *io.BinWriter) { + w.WriteString(e.Category) + w.WriteU32LE(e.ValidBlockStart) + w.WriteU32LE(e.ValidBlockEnd) + w.WriteBytes(e.Sender[:]) + w.WriteVarBytes(e.Data) +} + +// EncodeBinary implements io.Serializable. +func (e *Extensible) EncodeBinary(w *io.BinWriter) { + e.encodeBinaryUnsigned(w) + w.WriteB(1) + e.Witness.EncodeBinary(w) +} + +func (e *Extensible) decodeBinaryUnsigned(r *io.BinReader) { + e.Category = r.ReadString(maxExtensibleCategorySize) + e.ValidBlockStart = r.ReadU32LE() + e.ValidBlockEnd = r.ReadU32LE() + r.ReadBytes(e.Sender[:]) + e.Data = r.ReadVarBytes(maxExtensibleDataSize) +} + +// DecodeBinary implements io.Serializable. +func (e *Extensible) DecodeBinary(r *io.BinReader) { + e.decodeBinaryUnsigned(r) + if r.ReadB() != 1 { + if r.Err != nil { + return + } + r.Err = errInvalidPadding + return + } + e.Witness.DecodeBinary(r) +} + +// GetSignedPart implements crypto.Verifiable. +func (e *Extensible) GetSignedPart() []byte { + if e.signedpart == nil { + e.updateSignedPart() + } + return e.signedpart +} + +// GetSignedHash implements crypto.Verifiable. +func (e *Extensible) GetSignedHash() util.Uint256 { + if e.signedHash.Equals(util.Uint256{}) { + e.createHash() + } + return e.signedHash +} + +// Hash returns payload hash. +func (e *Extensible) Hash() util.Uint256 { + if e.hash.Equals(util.Uint256{}) { + e.createHash() + } + return e.hash +} + +// createHash creates hashes of the payload. +func (e *Extensible) createHash() { + b := e.GetSignedPart() + e.updateHashes(b) +} + +// updateHashes updates hashes based on the given buffer which should +// be a signable data slice. +func (e *Extensible) updateHashes(b []byte) { + e.signedHash = hash.Sha256(b) + e.hash = hash.Sha256(e.signedHash.BytesBE()) +} + +// updateSignedPart updates serialized message if needed. +func (e *Extensible) updateSignedPart() { + w := io.NewBufBinWriter() + e.encodeBinaryUnsigned(w.BinWriter) + e.signedpart = w.Bytes() +} diff --git a/pkg/network/payload/extensible_test.go b/pkg/network/payload/extensible_test.go new file mode 100644 index 000000000..846ed676c --- /dev/null +++ b/pkg/network/payload/extensible_test.go @@ -0,0 +1,71 @@ +package payload + +import ( + "errors" + gio "io" + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/stretchr/testify/require" +) + +func TestExtensible_Serializable(t *testing.T) { + expected := &Extensible{ + Category: "test", + ValidBlockStart: 12, + ValidBlockEnd: 1234, + Sender: random.Uint160(), + Data: random.Bytes(4), + Witness: transaction.Witness{ + InvocationScript: random.Bytes(3), + VerificationScript: random.Bytes(3), + }, + } + + testserdes.EncodeDecodeBinary(t, expected, new(Extensible)) + + t.Run("invalid", func(t *testing.T) { + w := io.NewBufBinWriter() + expected.encodeBinaryUnsigned(w.BinWriter) + unsigned := w.Bytes() + + t.Run("unexpected EOF", func(t *testing.T) { + err := testserdes.DecodeBinary(unsigned, new(Extensible)) + require.True(t, errors.Is(err, gio.EOF)) + }) + t.Run("invalid padding", func(t *testing.T) { + err := testserdes.DecodeBinary(append(unsigned, 42), new(Extensible)) + require.True(t, errors.Is(err, errInvalidPadding)) + }) + }) +} + +func TestExtensible_Hashes(t *testing.T) { + getExtensiblePair := func() (*Extensible, *Extensible) { + p1 := NewExtensible(netmode.UnitTestNet) + p1.Data = []byte{1, 2, 3} + p2 := NewExtensible(netmode.UnitTestNet) + p2.Data = []byte{3, 2, 1} + return p1, p2 + } + + t.Run("GetSignedPart", func(t *testing.T) { + p1, p2 := getExtensiblePair() + require.NotEqual(t, p1.GetSignedPart(), p2.GetSignedPart()) + require.NotEqual(t, p1.GetSignedPart(), p2.GetSignedPart()) + }) + t.Run("GetSignedHash", func(t *testing.T) { + p1, p2 := getExtensiblePair() + require.NotEqual(t, p1.GetSignedHash(), p2.GetSignedHash()) + require.NotEqual(t, p1.GetSignedHash(), p2.GetSignedHash()) + }) + t.Run("Hash", func(t *testing.T) { + p1, p2 := getExtensiblePair() + require.NotEqual(t, p1.Hash(), p2.Hash()) + require.NotEqual(t, p1.Hash(), p2.Hash()) + }) +} diff --git a/pkg/network/payload/inventory.go b/pkg/network/payload/inventory.go index 8727e2929..563ccdde1 100644 --- a/pkg/network/payload/inventory.go +++ b/pkg/network/payload/inventory.go @@ -18,8 +18,8 @@ func (i InventoryType) String() string { return "TX" case BlockType: return "block" - case ConsensusType: - return "consensus" + case ExtensibleType: + return "extensible" case P2PNotaryRequestType: return "p2pNotaryRequest" default: @@ -29,14 +29,14 @@ func (i InventoryType) String() string { // Valid returns true if the inventory (type) is known. func (i InventoryType) Valid(p2pSigExtensionsEnabled bool) bool { - return i == BlockType || i == TXType || i == ConsensusType || (p2pSigExtensionsEnabled && i == P2PNotaryRequestType) + return i == BlockType || i == TXType || i == ExtensibleType || (p2pSigExtensionsEnabled && i == P2PNotaryRequestType) } // List of valid InventoryTypes. const ( TXType InventoryType = 0x2b BlockType InventoryType = 0x2c - ConsensusType InventoryType = 0x2d + ExtensibleType InventoryType = 0x2e P2PNotaryRequestType InventoryType = 0x50 ) diff --git a/pkg/network/payload/inventory_test.go b/pkg/network/payload/inventory_test.go index 7d3920f9e..3b684367b 100644 --- a/pkg/network/payload/inventory_test.go +++ b/pkg/network/payload/inventory_test.go @@ -35,8 +35,8 @@ func TestValid(t *testing.T) { require.True(t, TXType.Valid(true)) require.True(t, BlockType.Valid(false)) require.True(t, BlockType.Valid(true)) - require.True(t, ConsensusType.Valid(false)) - require.True(t, ConsensusType.Valid(true)) + require.True(t, ExtensibleType.Valid(false)) + require.True(t, ExtensibleType.Valid(true)) require.False(t, P2PNotaryRequestType.Valid(false)) require.True(t, P2PNotaryRequestType.Valid(true)) require.False(t, InventoryType(0xFF).Valid(false)) @@ -46,7 +46,7 @@ func TestValid(t *testing.T) { func TestString(t *testing.T) { require.Equal(t, "TX", TXType.String()) require.Equal(t, "block", BlockType.String()) - require.Equal(t, "consensus", ConsensusType.String()) + require.Equal(t, "extensible", ExtensibleType.String()) require.Equal(t, "p2pNotaryRequest", P2PNotaryRequestType.String()) require.True(t, strings.Contains(InventoryType(0xFF).String(), "unknown")) } diff --git a/pkg/network/server.go b/pkg/network/server.go index b8befac11..8b5c1ea89 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -536,7 +536,7 @@ func (s *Server) handleInvCmd(p Peer, inv *payload.Inventory) error { var typExists = map[payload.InventoryType]func(util.Uint256) bool{ payload.TXType: s.chain.HasTransaction, payload.BlockType: s.chain.HasBlock, - payload.ConsensusType: func(h util.Uint256) bool { + payload.ExtensibleType: func(h util.Uint256) bool { cp := s.consensus.GetPayload(h) return cp != nil }, @@ -557,7 +557,7 @@ func (s *Server) handleInvCmd(p Peer, inv *payload.Inventory) error { if err != nil { return err } - if inv.Type == payload.ConsensusType { + if inv.Type == payload.ExtensibleType { return p.EnqueueHPPacket(true, pkt) } return p.EnqueueP2PPacket(pkt) @@ -605,9 +605,9 @@ func (s *Server) handleGetDataCmd(p Peer, inv *payload.Inventory) error { } else { notFound = append(notFound, hash) } - case payload.ConsensusType: + case payload.ExtensibleType: if cp := s.consensus.GetPayload(hash); cp != nil { - msg = NewMessage(CMDConsensus, cp) + msg = NewMessage(CMDExtensible, cp) } case payload.P2PNotaryRequestType: if nrp, ok := s.notaryRequestPool.TryGetData(hash); ok { // already have checked P2PSigExtEnabled @@ -619,7 +619,7 @@ func (s *Server) handleGetDataCmd(p Peer, inv *payload.Inventory) error { if msg != nil { pkt, err := msg.Bytes() if err == nil { - if inv.Type == payload.ConsensusType { + if inv.Type == payload.ExtensibleType { err = p.EnqueueHPPacket(true, pkt) } else { err = p.EnqueueP2PPacket(pkt) @@ -715,10 +715,29 @@ func (s *Server) handleGetHeadersCmd(p Peer, gh *payload.GetBlockByIndex) error return p.EnqueueP2PMessage(msg) } -// handleConsensusCmd processes received consensus payload. -// It never returns an error. -func (s *Server) handleConsensusCmd(cp *consensus.Payload) error { - s.consensus.OnPayload(cp) +const extensibleVerifyMaxGAS = 2000000 + +// handleExtensibleCmd processes received extensible payload. +func (s *Server) handleExtensibleCmd(e *payload.Extensible) error { + if err := s.chain.VerifyWitness(e.Sender, e, &e.Witness, extensibleVerifyMaxGAS); err != nil { + return err + } + h := s.chain.BlockHeight() + if h < e.ValidBlockStart || e.ValidBlockEnd <= h { + // We can receive consensus payload for the last or next block + // which leads to unwanted node disconnect. + if e.ValidBlockEnd == h { + return nil + } + return errors.New("invalid height") + } + + switch e.Category { + case consensus.Category: + s.consensus.OnPayload(e) + default: + return errors.New("invalid category") + } return nil } @@ -895,9 +914,9 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { case CMDBlock: block := msg.Payload.(*block.Block) return s.handleBlockCmd(peer, block) - case CMDConsensus: - cp := msg.Payload.(*consensus.Payload) - return s.handleConsensusCmd(cp) + case CMDExtensible: + cp := msg.Payload.(*payload.Extensible) + return s.handleExtensibleCmd(cp) case CMDTX: tx := msg.Payload.(*transaction.Transaction) return s.handleTxCmd(tx) @@ -933,8 +952,8 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { return nil } -func (s *Server) handleNewPayload(p *consensus.Payload) { - msg := NewMessage(CMDInv, payload.NewInventory(payload.ConsensusType, []util.Uint256{p.Hash()})) +func (s *Server) handleNewPayload(p *payload.Extensible) { + msg := NewMessage(CMDInv, payload.NewInventory(payload.ExtensibleType, []util.Uint256{p.Hash()})) // It's high priority because it directly affects consensus process, // even though it's just an inv. s.broadcastHPMessage(msg) diff --git a/pkg/network/server_test.go b/pkg/network/server_test.go index 9e457f1a2..365c4fe71 100644 --- a/pkg/network/server_test.go +++ b/pkg/network/server_test.go @@ -31,7 +31,7 @@ import ( type fakeConsensus struct { started atomic.Bool stopped atomic.Bool - payloads []*consensus.Payload + payloads []*payload.Extensible txs []*transaction.Transaction } @@ -40,11 +40,11 @@ var _ consensus.Service = (*fakeConsensus)(nil) func newFakeConsensus(c consensus.Config) (consensus.Service, error) { return new(fakeConsensus), nil } -func (f *fakeConsensus) Start() { f.started.Store(true) } -func (f *fakeConsensus) Shutdown() { f.stopped.Store(true) } -func (f *fakeConsensus) OnPayload(p *consensus.Payload) { f.payloads = append(f.payloads, p) } -func (f *fakeConsensus) OnTransaction(tx *transaction.Transaction) { f.txs = append(f.txs, tx) } -func (f *fakeConsensus) GetPayload(h util.Uint256) *consensus.Payload { panic("implement me") } +func (f *fakeConsensus) Start() { f.started.Store(true) } +func (f *fakeConsensus) Shutdown() { f.stopped.Store(true) } +func (f *fakeConsensus) OnPayload(p *payload.Extensible) { f.payloads = append(f.payloads, p) } +func (f *fakeConsensus) OnTransaction(tx *transaction.Transaction) { f.txs = append(f.txs, tx) } +func (f *fakeConsensus) GetPayload(h util.Uint256) *payload.Extensible { panic("implement me") } func TestNewServer(t *testing.T) { bc := &testChain{} @@ -405,9 +405,48 @@ func TestConsensus(t *testing.T) { s, shutdown := startTestServer(t) defer shutdown() - pl := consensus.NewPayload(netmode.UnitTestNet, false) - s.testHandleMessage(t, nil, CMDConsensus, pl) - require.Contains(t, s.consensus.(*fakeConsensus).payloads, pl) + atomic2.StoreUint32(&s.chain.(*testChain).blockheight, 4) + p := newLocalPeer(t, s) + p.handshaked = true + + newConsensusMessage := func(start, end uint32) *Message { + pl := payload.NewExtensible(netmode.UnitTestNet) + pl.Category = consensus.Category + pl.ValidBlockStart = start + pl.ValidBlockEnd = end + return NewMessage(CMDExtensible, pl) + } + + s.chain.(*testChain).verifyWitnessF = func() error { return errors.New("invalid") } + msg := newConsensusMessage(0, s.chain.BlockHeight()+1) + require.Error(t, s.handleMessage(p, msg)) + + s.chain.(*testChain).verifyWitnessF = func() error { return nil } + require.NoError(t, s.handleMessage(p, msg)) + require.Contains(t, s.consensus.(*fakeConsensus).payloads, msg.Payload.(*payload.Extensible)) + + t.Run("small ValidUntilBlockEnd", func(t *testing.T) { + t.Run("current height", func(t *testing.T) { + msg := newConsensusMessage(0, s.chain.BlockHeight()) + require.NoError(t, s.handleMessage(p, msg)) + require.NotContains(t, s.consensus.(*fakeConsensus).payloads, msg.Payload.(*payload.Extensible)) + }) + t.Run("invalid", func(t *testing.T) { + msg := newConsensusMessage(0, s.chain.BlockHeight()-1) + require.Error(t, s.handleMessage(p, msg)) + }) + }) + t.Run("big ValidUntiLBlockStart", func(t *testing.T) { + msg := newConsensusMessage(s.chain.BlockHeight()+1, s.chain.BlockHeight()+2) + require.Error(t, s.handleMessage(p, msg)) + }) + t.Run("invalid category", func(t *testing.T) { + pl := payload.NewExtensible(netmode.UnitTestNet) + pl.Category = "invalid" + pl.ValidBlockEnd = s.chain.BlockHeight() + 1 + msg := NewMessage(CMDExtensible, pl) + require.Error(t, s.handleMessage(p, msg)) + }) } func TestTransaction(t *testing.T) { @@ -448,7 +487,7 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType, p.handshaked = true p.messageHandler = func(t *testing.T, msg *Message) { switch msg.Command { - case CMDTX, CMDBlock, CMDConsensus, CMDP2PNotaryRequest: + case CMDTX, CMDBlock, CMDExtensible, CMDP2PNotaryRequest: require.Equal(t, found, msg.Payload) recvResponse.Store(true) case CMDNotFound: