Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix/Notary request tracking #2459

Merged
merged 4 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Changelog for NeoFS Node
- `neo-go` RPC connection loss handling (#1337)
- Concurrent morph cache misses (#1248)
- Double voting for validators on IR startup (#2365)
- Skip unexpected notary events on notary request parsing step (#2315)

### Removed
- Deprecated `morph.rpc_endpoint` SN and `morph.endpoint.client` IR config sections (#2400)
Expand Down
6 changes: 4 additions & 2 deletions pkg/morph/event/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ type listener struct {
notificationHandlers map[scriptHashWithType][]Handler

listenNotary bool
notaryEventsPreparator NotaryPreparator
notaryEventsPreparator preparator
notaryParsers map[notaryRequestTypes]NotaryParser
notaryHandlers map[notaryRequestTypes]Handler
notaryMainTXSigner util.Uint160 // filter for notary subscription
Expand Down Expand Up @@ -328,7 +328,7 @@ func (l *listener) parseAndHandleNotary(nr *result.NotaryRequestEvent) {
notaryEvent, err := l.notaryEventsPreparator.Prepare(nr.NotaryRequest)
if err != nil {
switch {
case errors.Is(err, ErrTXAlreadyHandled):
case errors.Is(err, ErrTXAlreadyHandled) || errors.Is(err, ErrUnknownEvent):
case errors.Is(err, ErrMainTXExpired):
l.log.Warn("skip expired main TX notary event",
zap.String("error", err.Error()),
Expand Down Expand Up @@ -515,6 +515,8 @@ func (l *listener) SetNotaryParser(pi NotaryParserInfo) {
l.notaryParsers[pi.notaryRequestTypes] = pi.parser()
}

l.notaryEventsPreparator.allowNotaryEvent(pi.notaryScriptWithHash)

log.Info("registered new event parser")
}

Expand Down
75 changes: 62 additions & 13 deletions pkg/morph/event/notary_preparator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"encoding/binary"
"errors"
"fmt"
"sync"

lru "github.com/hashicorp/golang-lru/v2"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
Expand Down Expand Up @@ -42,6 +44,9 @@ var (

// ErrMainTXExpired is returned if received fallback TX is already valid.
ErrMainTXExpired = errors.New("received main tx has expired")

// ErrUnknownEvent is returned if main transaction is not expected.
ErrUnknownEvent = errors.New("received notary request is not allowed")
AnnaShaleva marked this conversation as resolved.
Show resolved Hide resolved
)

// BlockCounter must return block count of the network
Expand All @@ -50,7 +55,7 @@ type BlockCounter interface {
BlockCount() (res uint32, err error)
}

// PreparatorPrm groups the required parameters of the Preparator constructor.
// PreparatorPrm groups the required parameters of the preparator constructor.
type PreparatorPrm struct {
AlphaKeys client.AlphabetKeys

Expand All @@ -59,8 +64,9 @@ type PreparatorPrm struct {
BlockCounter BlockCounter
}

// Preparator implements NotaryPreparator interface.
type Preparator struct {
// preparator constructs NotaryEvent
// from the NotaryRequest event.
type preparator struct {
// contractSysCall contract call in NeoVM
contractSysCall []byte
// dummyInvocationScript is invocation script from TX that is not signed.
Expand All @@ -69,13 +75,21 @@ type Preparator struct {
alphaKeys client.AlphabetKeys

blockCounter BlockCounter

// cache for TX recursion; size limited because it is
// just an optimization, already handled TX are not gonna
// be signed once more anyway
alreadyHandledTXs *lru.Cache[util.Uint256, struct{}]

m *sync.RWMutex
allowedEvents map[notaryScriptWithHash]struct{}
}

// notaryPreparator inits and returns NotaryPreparator.
// notaryPreparator inits and returns preparator.
//
// Considered to be used for preparing notary request
// for parsing it by event.Listener.
func notaryPreparator(prm PreparatorPrm) NotaryPreparator {
func notaryPreparator(prm PreparatorPrm) preparator {
switch {
case prm.AlphaKeys == nil:
panic("alphabet keys source must not be nil")
Expand All @@ -88,11 +102,17 @@ func notaryPreparator(prm PreparatorPrm) NotaryPreparator {

dummyInvocationScript := append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...)

return Preparator{
const txCacheSize = 1000
cache, _ := lru.New[util.Uint256, struct{}](txCacheSize)

return preparator{
contractSysCall: contractSysCall,
dummyInvocationScript: dummyInvocationScript,
alphaKeys: prm.AlphaKeys,
blockCounter: prm.BlockCounter,
alreadyHandledTXs: cache,
m: &sync.RWMutex{},
allowedEvents: make(map[notaryScriptWithHash]struct{}),
}
}

Expand All @@ -103,7 +123,15 @@ func notaryPreparator(prm PreparatorPrm) NotaryPreparator {
// transaction is expected to be received one more time
// from the Notary service but already signed. This happens
// since every notary call is a new notary request in fact.
func (p Preparator) Prepare(nr *payload.P2PNotaryRequest) (NotaryEvent, error) {
//
// Returns ErrUnknownEvent if main transactions is not an
// expected contract call.
func (p preparator) Prepare(nr *payload.P2PNotaryRequest) (NotaryEvent, error) {
if _, ok := p.alreadyHandledTXs.Get(nr.MainTransaction.Hash()); ok {
// received already signed and sent TX
return nil, ErrTXAlreadyHandled
}

// notary request's main tx is expected to have
// three or four witnesses: one for proxy contract,
// one for alphabet multisignature, one optional for
Expand Down Expand Up @@ -192,6 +220,18 @@ func (p Preparator) Prepare(nr *payload.P2PNotaryRequest) (NotaryEvent, error) {

// retrieve contract's method
contractMethod := string(ops[opsLen-3].param)
eventType := NotaryTypeFromString(contractMethod)

p.m.RLock()
_, allowed := p.allowedEvents[notaryScriptWithHash{
scriptHashValue: scriptHashValue{contractHash},
notaryRequestType: notaryRequestType{eventType},
}]
p.m.RUnlock()

if !allowed {
return nil, ErrUnknownEvent
}

// check if there is a call flag(must be in range [0:15))
callFlag := callflag.CallFlag(ops[opsLen-4].code - opcode.PUSH0)
Expand All @@ -211,15 +251,24 @@ func (p Preparator) Prepare(nr *payload.P2PNotaryRequest) (NotaryEvent, error) {
args = args[:len(args)-2]
}

p.alreadyHandledTXs.Add(nr.MainTransaction.Hash(), struct{}{})

return parsedNotaryEvent{
hash: contractHash,
notaryType: NotaryTypeFromString(contractMethod),
notaryType: eventType,
params: args,
raw: nr,
}, nil
}

func (p Preparator) validateParameterOpcodes(ops []Op) error {
func (p preparator) allowNotaryEvent(filter notaryScriptWithHash) {
p.m.Lock()
defer p.m.Unlock()

p.allowedEvents[filter] = struct{}{}
}

func (p preparator) validateParameterOpcodes(ops []Op) error {
l := len(ops)

if ops[l-1].code != opcode.PACK {
Expand Down Expand Up @@ -283,7 +332,7 @@ func validateNestedArgs(expArgLen int64, ops []Op) error {
return nil
}

func (p Preparator) validateExpiration(fbTX *transaction.Transaction) error {
func (p preparator) validateExpiration(fbTX *transaction.Transaction) error {
if len(fbTX.Attributes) != 3 {
return errIncorrectFBAttributesAmount
}
Expand All @@ -310,7 +359,7 @@ func (p Preparator) validateExpiration(fbTX *transaction.Transaction) error {
return nil
}

func (p Preparator) validateCosigners(expected int, s []transaction.Signer, alphaKeys keys.PublicKeys) error {
func (p preparator) validateCosigners(expected int, s []transaction.Signer, alphaKeys keys.PublicKeys) error {
if len(s) != expected {
return errUnexpectedCosignersAmount
}
Expand All @@ -327,7 +376,7 @@ func (p Preparator) validateCosigners(expected int, s []transaction.Signer, alph
return nil
}

func (p Preparator) validateWitnesses(w []transaction.Witness, alphaKeys keys.PublicKeys, invokerWitness bool) error {
func (p preparator) validateWitnesses(w []transaction.Witness, alphaKeys keys.PublicKeys, invokerWitness bool) error {
// the first one(proxy contract) must have empty
// witnesses
if len(w[0].VerificationScript)+len(w[0].InvocationScript) != 0 {
Expand Down Expand Up @@ -363,7 +412,7 @@ func (p Preparator) validateWitnesses(w []transaction.Witness, alphaKeys keys.Pu
return nil
}

func (p Preparator) validateAttributes(aa []transaction.Attribute, alphaKeys keys.PublicKeys, invokerWitness bool) error {
func (p preparator) validateAttributes(aa []transaction.Attribute, alphaKeys keys.PublicKeys, invokerWitness bool) error {
// main tx must have exactly one attribute
if len(aa) != 1 {
return errIncorrectAttributesAmount
Expand Down
49 changes: 43 additions & 6 deletions pkg/morph/event/notary_preparator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package event

import (
"fmt"
"strconv"
"testing"

"github.com/nspcc-dev/neo-go/pkg/vm"
Expand All @@ -21,6 +22,8 @@ import (
"github.com/stretchr/testify/require"
)

const contractMethod = "test"

var (
alphaKeys keys.PublicKeys
wrongAlphaKeys keys.PublicKeys
Expand Down Expand Up @@ -63,12 +66,17 @@ func TestPrepare_IncorrectScript(t *testing.T) {
},
)

preparator.allowNotaryEvent(notaryScriptWithHash{
notaryRequestType: notaryRequestType{contractMethod},
scriptHashValue: scriptHashValue{scriptHash},
})

for _, dummyMultisig := range []bool{true, false} { // try both empty and dummy multisig/Notary invocation witness script
t.Run(fmt.Sprintf("not contract call, compat: %t", dummyMultisig), func(t *testing.T) {
bw := io.NewBufBinWriter()

emit.Int(bw.BinWriter, 4)
emit.String(bw.BinWriter, "test")
emit.String(bw.BinWriter, contractMethod)
emit.Bytes(bw.BinWriter, scriptHash.BytesBE())
emit.Syscall(bw.BinWriter, interopnames.SystemContractCallNative) // any != interopnames.SystemContractCall

Expand All @@ -83,7 +91,7 @@ func TestPrepare_IncorrectScript(t *testing.T) {
bw := io.NewBufBinWriter()

emit.Int(bw.BinWriter, -1)
emit.String(bw.BinWriter, "test")
emit.String(bw.BinWriter, contractMethod)
emit.Bytes(bw.BinWriter, scriptHash.BytesBE())
emit.Syscall(bw.BinWriter, interopnames.SystemContractCall)

Expand Down Expand Up @@ -186,7 +194,13 @@ func TestPrepare_IncorrectNR(t *testing.T) {
name: "incorrect main TX attribute amount",
addW: false,
mTX: mTX{
attrs: []transaction.Attribute{{}, {}},
attrs: []transaction.Attribute{{
Type: transaction.NotValidBeforeT,
Value: &transaction.NotaryAssisted{},
}, {
Type: transaction.NotValidBeforeT,
Value: &transaction.NotaryAssisted{},
}},
},
expErr: errIncorrectAttributesAmount,
},
Expand All @@ -196,6 +210,7 @@ func TestPrepare_IncorrectNR(t *testing.T) {
mTX: mTX{
attrs: []transaction.Attribute{
{
Type: transaction.NotaryAssistedT,
Value: &transaction.NotaryAssisted{
NKeys: uint8(len(alphaKeys) + 1),
},
Expand Down Expand Up @@ -371,6 +386,7 @@ func TestPrepare_IncorrectNR(t *testing.T) {
mTX: mTX{
attrs: []transaction.Attribute{
{
Type: transaction.NotaryAssistedT,
Value: &transaction.NotaryAssisted{
NKeys: uint8(len(alphaKeys) + 2),
},
Expand Down Expand Up @@ -440,14 +456,20 @@ func TestPrepare_CorrectNR(t *testing.T) {

for _, test := range tests {
for i := 0; i < 1; i++ { // run tests against 3 and 4 witness NR
for _, dummyMultisig := range []bool{true, false} { // run tests against empty and dummy multisig/Notary witness
for j, dummyMultisig := range []bool{true, false} { // run tests against empty and dummy multisig/Notary witness
method := test.method + strconv.FormatInt(int64(j), 10)
preparator.allowNotaryEvent(notaryScriptWithHash{
notaryRequestType: notaryRequestType{NotaryTypeFromString(method)},
scriptHashValue: scriptHashValue{scriptHash},
})

additionalWitness := i == 0
nr := correctNR(script(test.hash, test.method, test.args...), dummyMultisig, additionalWitness)
nr := correctNR(script(test.hash, method, test.args...), dummyMultisig, additionalWitness)

event, err := preparator.Prepare(nr)

require.NoError(t, err)
require.Equal(t, test.method, event.Type().String())
require.Equal(t, method, event.Type().String())
require.Equal(t, test.hash.StringLE(), event.ScriptHash().StringLE())

// check args parsing
Expand Down Expand Up @@ -480,6 +502,20 @@ func TestPrepare_CorrectNR(t *testing.T) {
}
}

func TestNotAllowedEvents(t *testing.T) {
preparator := notaryPreparator(
PreparatorPrm{
alphaKeysSource(),
blockCounter{100, nil},
},
)

nr := correctNR(script(scriptHash, "test1", nil), false, false)

_, err := preparator.Prepare(nr)
require.ErrorIs(t, err, ErrUnknownEvent)
}

func alphaKeysSource() client.AlphabetKeys {
return func() (keys.PublicKeys, error) {
return alphaKeys, nil
Expand Down Expand Up @@ -546,6 +582,7 @@ func correctNR(script []byte, dummyMultisig, additionalWitness bool) *payload.P2
Scripts: scripts,
Attributes: []transaction.Attribute{
{
Type: transaction.NotaryAssistedT,
Value: &transaction.NotaryAssisted{
NKeys: nKeys,
},
Expand Down
7 changes: 0 additions & 7 deletions pkg/morph/event/parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"

"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
)

// NotificationParser is a function that constructs Event
Expand All @@ -20,12 +19,6 @@ type NotificationParserInfo struct {
p NotificationParser
}

// NotaryPreparator constructs NotaryEvent
// from the NotaryRequest event.
type NotaryPreparator interface {
Prepare(*payload.P2PNotaryRequest) (NotaryEvent, error)
}

// NotaryParser is a function that constructs Event
// from the NotaryEvent event.
type NotaryParser func(NotaryEvent) (Event, error)
Expand Down
4 changes: 4 additions & 0 deletions pkg/morph/event/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ type scriptHashWithType struct {

type notaryRequestTypes struct {
notaryRequestMempoolType
notaryScriptWithHash
}

type notaryScriptWithHash struct {
notaryRequestType
scriptHashValue
}
Expand Down
Loading