diff --git a/internal/fakechain/fakechain.go b/internal/fakechain/fakechain.go new file mode 100644 index 000000000..3b79ebc8b --- /dev/null +++ b/internal/fakechain/fakechain.go @@ -0,0 +1,438 @@ +package fakechain + +import ( + "errors" + "math/big" + "sync/atomic" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/block" + "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/interop" + "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "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/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" +) + +// FakeChain implements Blockchainer interface, but does not provide real functionality. +type FakeChain struct { + config.ProtocolConfiguration + *mempool.Pool + blocksCh []chan<- *block.Block + Blockheight uint32 + PoolTxF func(*transaction.Transaction) error + poolTxWithData func(*transaction.Transaction, interface{}, *mempool.Pool) error + blocks map[util.Uint256]*block.Block + hdrHashes map[uint32]util.Uint256 + txs map[util.Uint256]*transaction.Transaction + VerifyWitnessF func() error + MaxVerificationGAS int64 + NotaryContractScriptHash util.Uint160 + NotaryDepositExpiration uint32 + PostBlock []func(blockchainer.Blockchainer, *mempool.Pool, *block.Block) + UtilityTokenBalance *big.Int +} + +// NewFakeChain returns new FakeChain structure. +func NewFakeChain() *FakeChain { + return &FakeChain{ + Pool: mempool.New(10, 0, false), + PoolTxF: func(*transaction.Transaction) error { return nil }, + poolTxWithData: func(*transaction.Transaction, interface{}, *mempool.Pool) error { return nil }, + blocks: make(map[util.Uint256]*block.Block), + hdrHashes: make(map[uint32]util.Uint256), + txs: make(map[util.Uint256]*transaction.Transaction), + ProtocolConfiguration: config.ProtocolConfiguration{P2PNotaryRequestPayloadPoolSize: 10}, + } +} + +// PutBlock implements Blockchainer interface. +func (chain *FakeChain) PutBlock(b *block.Block) { + chain.blocks[b.Hash()] = b + chain.hdrHashes[b.Index] = b.Hash() + atomic.StoreUint32(&chain.Blockheight, b.Index) +} + +// PutHeader implements Blockchainer interface. +func (chain *FakeChain) PutHeader(b *block.Block) { + chain.hdrHashes[b.Index] = b.Hash() +} + +// PutTx implements Blockchainer interface. +func (chain *FakeChain) PutTx(tx *transaction.Transaction) { + chain.txs[tx.Hash()] = tx +} + +// ApplyPolicyToTxSet implements Blockchainer interface. +func (chain *FakeChain) ApplyPolicyToTxSet([]*transaction.Transaction) []*transaction.Transaction { + panic("TODO") +} + +// IsTxStillRelevant implements Blockchainer interface. +func (chain *FakeChain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool { + panic("TODO") +} + +// IsExtensibleAllowed implements Blockchainer interface. +func (*FakeChain) IsExtensibleAllowed(uint160 util.Uint160) bool { + return true +} + +// GetNotaryDepositExpiration implements Blockchainer interface. +func (chain *FakeChain) GetNotaryDepositExpiration(acc util.Uint160) uint32 { + if chain.NotaryDepositExpiration != 0 { + return chain.NotaryDepositExpiration + } + panic("TODO") +} + +// GetNotaryContractScriptHash implements Blockchainer interface. +func (chain *FakeChain) GetNotaryContractScriptHash() util.Uint160 { + if !chain.NotaryContractScriptHash.Equals(util.Uint160{}) { + return chain.NotaryContractScriptHash + } + panic("TODO") +} + +// GetNotaryBalance implements Blockchainer interface. +func (chain *FakeChain) GetNotaryBalance(acc util.Uint160) *big.Int { + panic("TODO") +} + +// GetPolicer implements Blockchainer interface. +func (chain *FakeChain) GetPolicer() blockchainer.Policer { + return chain +} + +// GetBaseExecFee implements Policer interface. +func (chain *FakeChain) GetBaseExecFee() int64 { + return interop.DefaultBaseExecFee +} + +// GetStoragePrice implements Policer interface. +func (chain *FakeChain) GetStoragePrice() int64 { + return native.StoragePrice +} + +// GetMaxVerificationGAS implements Policer interface. +func (chain *FakeChain) GetMaxVerificationGAS() int64 { + if chain.MaxVerificationGAS != 0 { + return chain.MaxVerificationGAS + } + panic("TODO") +} + +// PoolTxWithData implements Blockchainer interface. +func (chain *FakeChain) PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc blockchainer.Blockchainer, t *transaction.Transaction, data interface{}) error) error { + return chain.poolTxWithData(t, data, mp) +} + +// RegisterPostBlock implements Blockchainer interface. +func (chain *FakeChain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) { + chain.PostBlock = append(chain.PostBlock, f) +} + +// GetConfig implements Blockchainer interface. +func (chain *FakeChain) GetConfig() config.ProtocolConfiguration { + return chain.ProtocolConfiguration +} + +// CalculateClaimable implements Blockchainer interface. +func (chain *FakeChain) CalculateClaimable(util.Uint160, uint32) (*big.Int, error) { + panic("TODO") +} + +// FeePerByte implements Feer interface. +func (chain *FakeChain) FeePerByte() int64 { + panic("TODO") +} + +// P2PSigExtensionsEnabled implements Feer interface. +func (chain *FakeChain) P2PSigExtensionsEnabled() bool { + return true +} + +// GetMaxBlockSystemFee implements Policer interface. +func (chain *FakeChain) GetMaxBlockSystemFee() int64 { + panic("TODO") +} + +// GetMaxBlockSize implements Policer interface. +func (chain *FakeChain) GetMaxBlockSize() uint32 { + panic("TODO") +} + +// AddHeaders implements Blockchainer interface. +func (chain *FakeChain) AddHeaders(...*block.Header) error { + panic("TODO") +} + +// AddBlock implements Blockchainer interface. +func (chain *FakeChain) AddBlock(block *block.Block) error { + if block.Index == atomic.LoadUint32(&chain.Blockheight)+1 { + chain.PutBlock(block) + } + return nil +} + +// AddStateRoot implements Blockchainer interface. +func (chain *FakeChain) AddStateRoot(r *state.MPTRoot) error { + panic("TODO") +} + +// BlockHeight implements Feer interface. +func (chain *FakeChain) BlockHeight() uint32 { + return atomic.LoadUint32(&chain.Blockheight) +} + +// Close implements Blockchainer interface. +func (chain *FakeChain) Close() { + panic("TODO") +} + +// HeaderHeight implements Blockchainer interface. +func (chain *FakeChain) HeaderHeight() uint32 { + return atomic.LoadUint32(&chain.Blockheight) +} + +// GetAppExecResults implements Blockchainer interface. +func (chain *FakeChain) GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) { + panic("TODO") +} + +// GetBlock implements Blockchainer interface. +func (chain *FakeChain) GetBlock(hash util.Uint256) (*block.Block, error) { + if b, ok := chain.blocks[hash]; ok { + return b, nil + } + return nil, errors.New("not found") +} + +// GetCommittee implements Blockchainer interface. +func (chain *FakeChain) GetCommittee() (keys.PublicKeys, error) { + panic("TODO") +} + +// GetContractState implements Blockchainer interface. +func (chain *FakeChain) GetContractState(hash util.Uint160) *state.Contract { + panic("TODO") +} + +// GetContractScriptHash implements Blockchainer interface. +func (chain *FakeChain) GetContractScriptHash(id int32) (util.Uint160, error) { + panic("TODO") +} + +// GetNativeContractScriptHash implements Blockchainer interface. +func (chain *FakeChain) GetNativeContractScriptHash(name string) (util.Uint160, error) { + panic("TODO") +} + +// GetHeaderHash implements Blockchainer interface. +func (chain *FakeChain) GetHeaderHash(n int) util.Uint256 { + return chain.hdrHashes[uint32(n)] +} + +// GetHeader implements Blockchainer interface. +func (chain *FakeChain) GetHeader(hash util.Uint256) (*block.Header, error) { + b, err := chain.GetBlock(hash) + if err != nil { + return nil, err + } + return b.Header(), nil +} + +// GetNextBlockValidators implements Blockchainer interface. +func (chain *FakeChain) GetNextBlockValidators() ([]*keys.PublicKey, error) { + panic("TODO") +} + +// ForEachNEP17Transfer implements Blockchainer interface. +func (chain *FakeChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error { + panic("TODO") +} + +// GetNEP17Balances implements Blockchainer interface. +func (chain *FakeChain) GetNEP17Balances(util.Uint160) *state.NEP17Balances { + panic("TODO") +} + +// GetValidators implements Blockchainer interface. +func (chain *FakeChain) GetValidators() ([]*keys.PublicKey, error) { + panic("TODO") +} + +// GetStandByCommittee implements Blockchainer interface. +func (chain *FakeChain) GetStandByCommittee() keys.PublicKeys { + panic("TODO") +} + +// GetStandByValidators implements Blockchainer interface. +func (chain *FakeChain) GetStandByValidators() keys.PublicKeys { + panic("TODO") +} + +// GetEnrollments implements Blockchainer interface. +func (chain *FakeChain) GetEnrollments() ([]state.Validator, error) { + panic("TODO") +} + +// GetStateProof implements Blockchainer interface. +func (chain *FakeChain) GetStateProof(util.Uint256, []byte) ([][]byte, error) { + panic("TODO") +} + +// GetStateRoot implements Blockchainer interface. +func (chain *FakeChain) GetStateRoot(height uint32) (*state.MPTRootState, error) { + panic("TODO") +} + +// GetStorageItem implements Blockchainer interface. +func (chain *FakeChain) GetStorageItem(id int32, key []byte) *state.StorageItem { + panic("TODO") +} + +// GetTestVM implements Blockchainer interface. +func (chain *FakeChain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *vm.VM { + panic("TODO") +} + +// GetStorageItems implements Blockchainer interface. +func (chain *FakeChain) GetStorageItems(id int32) (map[string]*state.StorageItem, error) { + panic("TODO") +} + +// CurrentHeaderHash implements Blockchainer interface. +func (chain *FakeChain) CurrentHeaderHash() util.Uint256 { + return util.Uint256{} +} + +// CurrentBlockHash implements Blockchainer interface. +func (chain *FakeChain) CurrentBlockHash() util.Uint256 { + return util.Uint256{} +} + +// HasBlock implements Blockchainer interface. +func (chain *FakeChain) HasBlock(h util.Uint256) bool { + _, ok := chain.blocks[h] + return ok +} + +// HasTransaction implements Blockchainer interface. +func (chain *FakeChain) HasTransaction(h util.Uint256) bool { + _, ok := chain.txs[h] + return ok +} + +// GetTransaction implements Blockchainer interface. +func (chain *FakeChain) GetTransaction(h util.Uint256) (*transaction.Transaction, uint32, error) { + if tx, ok := chain.txs[h]; ok { + return tx, 1, nil + } + return nil, 0, errors.New("not found") +} + +// GetMemPool implements Blockchainer interface. +func (chain *FakeChain) GetMemPool() *mempool.Pool { + return chain.Pool +} + +// GetGoverningTokenBalance implements Blockchainer interface. +func (chain *FakeChain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) { + panic("TODO") +} + +// GetUtilityTokenBalance implements Feer interface. +func (chain *FakeChain) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int { + if chain.UtilityTokenBalance != nil { + return chain.UtilityTokenBalance + } + panic("TODO") +} + +// ManagementContractHash implements Blockchainer interface. +func (chain FakeChain) ManagementContractHash() util.Uint160 { + panic("TODO") +} + +// PoolTx implements Blockchainer interface. +func (chain *FakeChain) PoolTx(tx *transaction.Transaction, _ ...*mempool.Pool) error { + return chain.PoolTxF(tx) +} + +// SetOracle implements Blockchainer interface. +func (chain FakeChain) SetOracle(services.Oracle) { + panic("TODO") +} + +// SetNotary implements Blockchainer interface. +func (chain *FakeChain) SetNotary(notary services.Notary) { + panic("TODO") +} + +// SubscribeForBlocks implements Blockchainer interface. +func (chain *FakeChain) SubscribeForBlocks(ch chan<- *block.Block) { + chain.blocksCh = append(chain.blocksCh, ch) +} + +// SubscribeForExecutions implements Blockchainer interface. +func (chain *FakeChain) SubscribeForExecutions(ch chan<- *state.AppExecResult) { + panic("TODO") +} + +// SubscribeForNotifications implements Blockchainer interface. +func (chain *FakeChain) SubscribeForNotifications(ch chan<- *state.NotificationEvent) { + panic("TODO") +} + +// SubscribeForTransactions implements Blockchainer interface. +func (chain *FakeChain) SubscribeForTransactions(ch chan<- *transaction.Transaction) { + panic("TODO") +} + +// VerifyTx implements Blockchainer interface. +func (chain *FakeChain) VerifyTx(*transaction.Transaction) error { + panic("TODO") +} + +// VerifyWitness implements Blockchainer interface. +func (chain *FakeChain) VerifyWitness(util.Uint160, crypto.Verifiable, *transaction.Witness, int64) error { + if chain.VerifyWitnessF != nil { + return chain.VerifyWitnessF() + } + panic("TODO") +} + +// UnsubscribeFromBlocks implements Blockchainer interface. +func (chain *FakeChain) UnsubscribeFromBlocks(ch chan<- *block.Block) { + for i, c := range chain.blocksCh { + if c == ch { + if i < len(chain.blocksCh) { + copy(chain.blocksCh[i:], chain.blocksCh[i+1:]) + } + chain.blocksCh = chain.blocksCh[:len(chain.blocksCh)] + } + } +} + +// UnsubscribeFromExecutions implements Blockchainer interface. +func (chain *FakeChain) UnsubscribeFromExecutions(ch chan<- *state.AppExecResult) { + panic("TODO") +} + +// UnsubscribeFromNotifications implements Blockchainer interface. +func (chain *FakeChain) UnsubscribeFromNotifications(ch chan<- *state.NotificationEvent) { + panic("TODO") +} + +// UnsubscribeFromTransactions implements Blockchainer interface. +func (chain *FakeChain) UnsubscribeFromTransactions(ch chan<- *transaction.Transaction) { + panic("TODO") +} diff --git a/pkg/config/notary_config.go b/pkg/config/notary_config.go new file mode 100644 index 000000000..791ed8f86 --- /dev/null +++ b/pkg/config/notary_config.go @@ -0,0 +1,7 @@ +package config + +// P2PNotary stores configuration for Notary node service. +type P2PNotary struct { + Enabled bool `yaml:"Enabled"` + UnlockWallet Wallet `yaml:"UnlockWallet"` +} diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index b1860b8c4..c19fc53f2 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -20,6 +20,8 @@ type ( RemoveUntraceableBlocks bool `yaml:"RemoveUntraceableBlocks"` // MaxTraceableBlocks is the length of the chain accessible to smart contracts. MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"` + // P2PNotary stores configuration for P2P notary node service + P2PNotary P2PNotary `yaml:"P2PNotary"` // P2PSigExtensions enables additional signature-related logic. P2PSigExtensions bool `yaml:"P2PSigExtensions"` // ReservedAttributes allows to have reserved attributes range for experimental or private purposes. diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index a35230f02..3e31d6ecf 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -451,7 +451,7 @@ func (s *service) verifyBlock(b block.Block) bool { } var fee int64 - var pool = mempool.New(len(coreb.Transactions), 0) + var pool = mempool.New(len(coreb.Transactions), 0, false) var mainPool = s.Chain.GetMemPool() for _, tx := range coreb.Transactions { var err error diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 0b7f4e336..20481bd02 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -177,7 +177,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L dao: dao.NewSimple(s, cfg.Magic, cfg.StateRootInHeader), stopCh: make(chan struct{}), runToExitCh: make(chan struct{}), - memPool: mempool.New(cfg.MemPoolSize, 0), + memPool: mempool.New(cfg.MemPoolSize, 0, false), sbCommittee: committee, log: log, events: make(chan bcEvent), @@ -201,6 +201,12 @@ func (bc *Blockchain) SetOracle(mod services.Oracle) { bc.contracts.Designate.OracleService.Store(mod) } +// SetNotary sets notary module. It doesn't protected by mutex and +// must be called before `bc.Run()` to avoid data race. +func (bc *Blockchain) SetNotary(mod services.Notary) { + bc.contracts.Designate.NotaryService.Store(mod) +} + func (bc *Blockchain) init() error { // If we could not find the version in the Store, we know that there is nothing stored. ver, err := bc.dao.GetVersion() @@ -477,7 +483,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error { if !block.MerkleRoot.Equals(merkle) { return errors.New("invalid block: MerkleRoot mismatch") } - mp = mempool.New(len(block.Transactions), 0) + mp = mempool.New(len(block.Transactions), 0, false) for _, tx := range block.Transactions { var err error // Transactions are verified before adding them @@ -1637,7 +1643,7 @@ func (bc *Blockchain) verifyStateRootWitness(r *state.MPTRoot) error { // current blockchain state. Note that this verification is completely isolated // from the main node's mempool. func (bc *Blockchain) VerifyTx(t *transaction.Transaction) error { - var mp = mempool.New(1, 0) + var mp = mempool.New(1, 0, false) bc.lock.RLock() defer bc.lock.RUnlock() return bc.verifyAndPoolTx(t, mp, bc) diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 4b4741b91..624eb5d05 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -449,7 +449,7 @@ func TestVerifyTx(t *testing.T) { require.True(t, errors.Is(err, ErrAlreadyExists)) }) t.Run("MemPoolOOM", func(t *testing.T) { - bc.memPool = mempool.New(1, 0) + bc.memPool = mempool.New(1, 0, false) tx1 := bc.newTestTx(h, testScript) tx1.NetworkFee += 10000 // Give it more priority. require.NoError(t, accs[0].SignTx(tx1)) @@ -988,7 +988,7 @@ func TestVerifyTx(t *testing.T) { return tx } - mp := mempool.New(10, 1) + mp := mempool.New(10, 1, false) verificationF := func(bc blockchainer.Blockchainer, tx *transaction.Transaction, data interface{}) error { if data.(int) > 5 { return errors.New("bad data") diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 99a151c11..9dad9966c 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -65,6 +65,7 @@ type Blockchainer interface { PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) error PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc Blockchainer, t *transaction.Transaction, data interface{}) error) error RegisterPostBlock(f func(Blockchainer, *mempool.Pool, *block.Block)) + SetNotary(mod services.Notary) SubscribeForBlocks(ch chan<- *block.Block) SubscribeForExecutions(ch chan<- *state.AppExecResult) SubscribeForNotifications(ch chan<- *state.NotificationEvent) diff --git a/pkg/core/blockchainer/services/notary.go b/pkg/core/blockchainer/services/notary.go new file mode 100644 index 000000000..76ccd8a7d --- /dev/null +++ b/pkg/core/blockchainer/services/notary.go @@ -0,0 +1,8 @@ +package services + +import "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + +// Notary is a Notary module interface. +type Notary interface { + UpdateNotaryNodes(pubs keys.PublicKeys) +} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 9545c8499..3b48f1841 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native" @@ -399,6 +400,9 @@ func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs .. w := io.NewBufBinWriter() emit.AppCall(w.BinWriter, sc, "transfer", callflag.All, from, to, amount, additionalArgs) emit.Opcodes(w.BinWriter, opcode.ASSERT) + if w.Err != nil { + panic(fmt.Errorf("failed to create nep17 transfer transaction: %w", w.Err)) + } script := w.Bytes() return transaction.New(testchain.Network(), script, 11000000) @@ -555,6 +559,14 @@ func invokeContractMethodBy(t *testing.T, chain *Blockchain, signer *wallet.Acco return invokeContractMethodGeneric(chain, sysfee, hash, method, signer, args...) } +func transferTokenFromMultisigAccountCheckOK(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) { + transferTx := transferTokenFromMultisigAccount(t, chain, to, tokenHash, amount, additionalArgs...) + res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application) + require.NoError(t, err) + require.Equal(t, vm.HaltState, res[0].VMState) + require.Equal(t, 0, len(res[0].Stack)) +} + func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction { transferTx := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...) transferTx.SystemFee = 100000000 @@ -580,3 +592,19 @@ func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ContractID] require.Equal(t, int64(expected), balance.Balance.Int64()) } + +type NotaryFeerStub struct { + bc blockchainer.Blockchainer +} + +func (f NotaryFeerStub) FeePerByte() int64 { return f.bc.FeePerByte() } +func (f NotaryFeerStub) GetUtilityTokenBalance(acc util.Uint160) *big.Int { + return f.bc.GetNotaryBalance(acc) +} +func (f NotaryFeerStub) BlockHeight() uint32 { return f.bc.BlockHeight() } +func (f NotaryFeerStub) P2PSigExtensionsEnabled() bool { return f.bc.P2PSigExtensionsEnabled() } +func NewNotaryFeerStub(bc blockchainer.Blockchainer) NotaryFeerStub { + return NotaryFeerStub{ + bc: bc, + } +} diff --git a/pkg/core/mempool/mem_pool.go b/pkg/core/mempool/mem_pool.go index 1741feba0..1215b9f52 100644 --- a/pkg/core/mempool/mem_pool.go +++ b/pkg/core/mempool/mem_pool.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/util" + "go.uber.org/atomic" ) var ( @@ -69,6 +70,14 @@ type Pool struct { resendThreshold uint32 resendFunc func(*transaction.Transaction, interface{}) + + // subscriptions for mempool events + subscriptionsEnabled bool + subscriptionsOn atomic.Bool + stopCh chan struct{} + events chan Event + subCh chan chan<- Event // there are no other events in mempool except Event, so no need in generic subscribers type + unsubCh chan chan<- Event } func (p items) Len() int { return len(p) } @@ -249,6 +258,13 @@ func (mp *Pool) Add(t *transaction.Transaction, fee Feer, data ...interface{}) e delete(mp.oracleResp, attrs[0].Value.(*transaction.OracleResponse).ID) } mp.verifiedTxes[len(mp.verifiedTxes)-1] = pItem + if mp.subscriptionsOn.Load() { + mp.events <- Event{ + Type: TransactionRemoved, + Tx: unlucky.txn, + Data: unlucky.data, + } + } } else { mp.verifiedTxes = append(mp.verifiedTxes, pItem) } @@ -269,6 +285,14 @@ func (mp *Pool) Add(t *transaction.Transaction, fee Feer, data ...interface{}) e updateMempoolMetrics(len(mp.verifiedTxes)) mp.lock.Unlock() + + if mp.subscriptionsOn.Load() { + mp.events <- Event{ + Type: TransactionAdded, + Tx: pItem.txn, + Data: pItem.data, + } + } return nil } @@ -307,6 +331,13 @@ func (mp *Pool) removeInternal(hash util.Uint256, feer Feer) { if attrs := tx.GetAttributes(transaction.OracleResponseT); len(attrs) != 0 { delete(mp.oracleResp, attrs[0].Value.(*transaction.OracleResponse).ID) } + if mp.subscriptionsOn.Load() { + mp.events <- Event{ + Type: TransactionRemoved, + Tx: itm.txn, + Data: itm.data, + } + } } updateMempoolMetrics(len(mp.verifiedTxes)) } @@ -325,7 +356,9 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) mp.conflicts = make(map[util.Uint256][]util.Uint256) } height := feer.BlockHeight() - var staleItems []item + var ( + staleItems []item + ) for _, itm := range mp.verifiedTxes { if isOK(itm.txn) && mp.checkPolicy(itm.txn, policyChanged) && mp.tryAddSendersFee(itm.txn, feer, true) { newVerifiedTxes = append(newVerifiedTxes, itm) @@ -348,6 +381,13 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) if attrs := itm.txn.GetAttributes(transaction.OracleResponseT); len(attrs) != 0 { delete(mp.oracleResp, attrs[0].Value.(*transaction.OracleResponse).ID) } + if mp.subscriptionsOn.Load() { + mp.events <- Event{ + Type: TransactionRemoved, + Tx: itm.txn, + Data: itm.data, + } + } } } if len(staleItems) != 0 { @@ -377,16 +417,23 @@ func (mp *Pool) checkPolicy(tx *transaction.Transaction, policyChanged bool) boo } // New returns a new Pool struct. -func New(capacity int, payerIndex int) *Pool { - return &Pool{ - verifiedMap: make(map[util.Uint256]*transaction.Transaction), - verifiedTxes: make([]item, 0, capacity), - capacity: capacity, - payerIndex: payerIndex, - fees: make(map[util.Uint160]utilityBalanceAndFees), - conflicts: make(map[util.Uint256][]util.Uint256), - oracleResp: make(map[uint64]util.Uint256), +func New(capacity int, payerIndex int, enableSubscriptions bool) *Pool { + mp := &Pool{ + verifiedMap: make(map[util.Uint256]*transaction.Transaction), + verifiedTxes: make([]item, 0, capacity), + capacity: capacity, + payerIndex: payerIndex, + fees: make(map[util.Uint160]utilityBalanceAndFees), + conflicts: make(map[util.Uint256][]util.Uint256), + oracleResp: make(map[uint64]util.Uint256), + subscriptionsEnabled: enableSubscriptions, + stopCh: make(chan struct{}), + events: make(chan Event), + subCh: make(chan chan<- Event), + unsubCh: make(chan chan<- Event), } + mp.subscriptionsOn.Store(false) + return mp } // SetResendThreshold sets threshold after which transaction will be considered stale diff --git a/pkg/core/mempool/mem_pool_test.go b/pkg/core/mempool/mem_pool_test.go index be056b828..c7b79ef80 100644 --- a/pkg/core/mempool/mem_pool_test.go +++ b/pkg/core/mempool/mem_pool_test.go @@ -45,7 +45,7 @@ func (fs *FeerStub) P2PSigExtensionsEnabled() bool { } func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) { - mp := New(10, 0) + mp := New(10, 0, false) tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx.Nonce = 0 tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}} @@ -66,7 +66,7 @@ func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) { } func TestMemPoolRemoveStale(t *testing.T) { - mp := New(5, 0) + mp := New(5, 0, false) txs := make([]*transaction.Transaction, 5) for i := range txs { txs[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) @@ -117,7 +117,7 @@ func TestMemPoolAddRemove(t *testing.T) { func TestOverCapacity(t *testing.T) { var fs = &FeerStub{balance: 10000000} const mempoolSize = 10 - mp := New(mempoolSize, 0) + mp := New(mempoolSize, 0, false) for i := 0; i < mempoolSize; i++ { tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) @@ -193,7 +193,7 @@ func TestOverCapacity(t *testing.T) { func TestGetVerified(t *testing.T) { var fs = &FeerStub{} const mempoolSize = 10 - mp := New(mempoolSize, 0) + mp := New(mempoolSize, 0, false) txes := make([]*transaction.Transaction, 0, mempoolSize) for i := 0; i < mempoolSize; i++ { @@ -217,7 +217,7 @@ func TestGetVerified(t *testing.T) { func TestRemoveStale(t *testing.T) { var fs = &FeerStub{} const mempoolSize = 10 - mp := New(mempoolSize, 0) + mp := New(mempoolSize, 0, false) txes1 := make([]*transaction.Transaction, 0, mempoolSize/2) txes2 := make([]*transaction.Transaction, 0, mempoolSize/2) @@ -250,7 +250,7 @@ func TestRemoveStale(t *testing.T) { } func TestMemPoolFees(t *testing.T) { - mp := New(10, 0) + mp := New(10, 0, false) fs := &FeerStub{balance: 10000000} sender0 := util.Uint160{1, 2, 3} tx0 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) @@ -361,7 +361,7 @@ func TestMempoolItemsOrder(t *testing.T) { } func TestMempoolAddRemoveOracleResponse(t *testing.T) { - mp := New(3, 0) + mp := New(3, 0, false) nonce := uint32(0) fs := &FeerStub{balance: 10000} newTx := func(netFee int64, id uint64) *transaction.Transaction { @@ -431,7 +431,7 @@ func TestMempoolAddRemoveOracleResponse(t *testing.T) { func TestMempoolAddRemoveConflicts(t *testing.T) { capacity := 6 - mp := New(capacity, 0) + mp := New(capacity, 0, false) var ( fs = &FeerStub{p2pSigExt: true, balance: 100000} nonce uint32 = 1 @@ -561,7 +561,7 @@ func TestMempoolAddWithDataGetData(t *testing.T) { blockHeight: 5, balance: 100, } - mp := New(10, 1) + mp := New(10, 1, false) newTx := func(t *testing.T, netFee int64) *transaction.Transaction { tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.RET)}, 0) tx.Signers = []transaction.Signer{{}, {}} diff --git a/pkg/core/mempool/subscriptions.go b/pkg/core/mempool/subscriptions.go new file mode 100644 index 000000000..9e1f667a9 --- /dev/null +++ b/pkg/core/mempool/subscriptions.go @@ -0,0 +1,86 @@ +package mempool + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" +) + +// EventType represents mempool event type. +type EventType byte + +const ( + // TransactionAdded marks transaction addition mempool event. + TransactionAdded EventType = 0x01 + // TransactionRemoved marks transaction removal mempool event. + TransactionRemoved EventType = 0x02 +) + +// Event represents one of mempool events: transaction was added or removed from mempool. +type Event struct { + Type EventType + Tx *transaction.Transaction + Data interface{} +} + +// RunSubscriptions runs subscriptions goroutine if mempool subscriptions are enabled. +// You should manually free the resources by calling StopSubscriptions on mempool shutdown. +func (mp *Pool) RunSubscriptions() { + if !mp.subscriptionsEnabled { + panic("subscriptions are disabled") + } + if !mp.subscriptionsOn.Load() { + mp.subscriptionsOn.Store(true) + go mp.notificationDispatcher() + } +} + +// StopSubscriptions stops mempool events loop. +func (mp *Pool) StopSubscriptions() { + if !mp.subscriptionsEnabled { + panic("subscriptions are disabled") + } + if mp.subscriptionsOn.Load() { + mp.subscriptionsOn.Store(false) + close(mp.stopCh) + } +} + +// SubscribeForTransactions adds given channel to new mempool event broadcasting, so when +// there is a new transactions added to mempool or an existing transaction removed from +// mempool you'll receive it via this channel. +func (mp *Pool) SubscribeForTransactions(ch chan<- Event) { + if mp.subscriptionsOn.Load() { + mp.subCh <- ch + } +} + +// UnsubscribeFromTransactions unsubscribes given channel from new mempool notifications, +// you can close it afterwards. Passing non-subscribed channel is a no-op. +func (mp *Pool) UnsubscribeFromTransactions(ch chan<- Event) { + if mp.subscriptionsOn.Load() { + mp.unsubCh <- ch + } +} + +// notificationDispatcher manages subscription to events and broadcasts new events. +func (mp *Pool) notificationDispatcher() { + var ( + // These are just sets of subscribers, though modelled as maps + // for ease of management (not a lot of subscriptions is really + // expected, but maps are convenient for adding/deleting elements). + txFeed = make(map[chan<- Event]bool) + ) + for { + select { + case <-mp.stopCh: + return + case sub := <-mp.subCh: + txFeed[sub] = true + case unsub := <-mp.unsubCh: + delete(txFeed, unsub) + case event := <-mp.events: + for ch := range txFeed { + ch <- event + } + } + } +} diff --git a/pkg/core/mempool/subscriptions_test.go b/pkg/core/mempool/subscriptions_test.go new file mode 100644 index 000000000..5c123ec0e --- /dev/null +++ b/pkg/core/mempool/subscriptions_test.go @@ -0,0 +1,98 @@ +package mempool + +import ( + "testing" + "time" + + "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/require" +) + +func TestSubscriptions(t *testing.T) { + t.Run("disabled subscriptions", func(t *testing.T) { + mp := New(5, 0, false) + require.Panics(t, func() { + mp.RunSubscriptions() + }) + require.Panics(t, func() { + mp.StopSubscriptions() + }) + }) + + t.Run("enabled subscriptions", func(t *testing.T) { + fs := &FeerStub{balance: 100} + mp := New(2, 0, true) + mp.RunSubscriptions() + subChan1 := make(chan Event, 3) + subChan2 := make(chan Event, 3) + mp.SubscribeForTransactions(subChan1) + defer mp.StopSubscriptions() + + txs := make([]*transaction.Transaction, 4) + for i := range txs { + txs[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) + txs[i].Nonce = uint32(i) + txs[i].Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}} + txs[i].NetworkFee = int64(i) + } + + // add tx + require.NoError(t, mp.Add(txs[0], fs)) + require.Eventually(t, func() bool { return len(subChan1) == 1 }, time.Second, time.Millisecond*100) + event := <-subChan1 + require.Equal(t, Event{Type: TransactionAdded, Tx: txs[0]}, event) + + // severak subscribers + mp.SubscribeForTransactions(subChan2) + require.NoError(t, mp.Add(txs[1], fs)) + require.Eventually(t, func() bool { return len(subChan1) == 1 && len(subChan2) == 1 }, time.Second, time.Millisecond*100) + event1 := <-subChan1 + event2 := <-subChan2 + require.Equal(t, Event{Type: TransactionAdded, Tx: txs[1]}, event1) + require.Equal(t, Event{Type: TransactionAdded, Tx: txs[1]}, event2) + + // reach capacity + require.NoError(t, mp.Add(txs[2], &FeerStub{})) + require.Eventually(t, func() bool { return len(subChan1) == 2 && len(subChan2) == 2 }, time.Second, time.Millisecond*100) + event1 = <-subChan1 + event2 = <-subChan2 + require.Equal(t, Event{Type: TransactionRemoved, Tx: txs[0]}, event1) + require.Equal(t, Event{Type: TransactionRemoved, Tx: txs[0]}, event2) + event1 = <-subChan1 + event2 = <-subChan2 + require.Equal(t, Event{Type: TransactionAdded, Tx: txs[2]}, event1) + require.Equal(t, Event{Type: TransactionAdded, Tx: txs[2]}, event2) + + // remove tx + mp.Remove(txs[1].Hash(), fs) + require.Eventually(t, func() bool { return len(subChan1) == 1 && len(subChan2) == 1 }, time.Second, time.Millisecond*100) + event1 = <-subChan1 + event2 = <-subChan2 + require.Equal(t, Event{Type: TransactionRemoved, Tx: txs[1]}, event1) + require.Equal(t, Event{Type: TransactionRemoved, Tx: txs[1]}, event2) + + // remove stale + mp.RemoveStale(func(tx *transaction.Transaction) bool { + if tx.Hash().Equals(txs[2].Hash()) { + return false + } + return true + }, fs) + require.Eventually(t, func() bool { return len(subChan1) == 1 && len(subChan2) == 1 }, time.Second, time.Millisecond*100) + event1 = <-subChan1 + event2 = <-subChan2 + require.Equal(t, Event{Type: TransactionRemoved, Tx: txs[2]}, event1) + require.Equal(t, Event{Type: TransactionRemoved, Tx: txs[2]}, event2) + + // unsubscribe + mp.UnsubscribeFromTransactions(subChan1) + require.NoError(t, mp.Add(txs[3], fs)) + require.Eventually(t, func() bool { return len(subChan2) == 1 }, time.Second, time.Millisecond*100) + event2 = <-subChan2 + require.Equal(t, 0, len(subChan1)) + require.Equal(t, Event{Type: TransactionAdded, Tx: txs[3]}, event2) + }) +} diff --git a/pkg/core/native/designate.go b/pkg/core/native/designate.go index d507fccec..768d5b177 100644 --- a/pkg/core/native/designate.go +++ b/pkg/core/native/designate.go @@ -37,6 +37,8 @@ type Designate struct { p2pSigExtensionsEnabled bool OracleService atomic.Value + // NotaryService represents Notary node module. + NotaryService atomic.Value } type roleData struct { @@ -183,10 +185,15 @@ func (s *Designate) updateCachedRoleData(v *atomic.Value, d dao.DAO, r Role) err addr: s.hashFromNodes(r, nodeKeys), height: height, }) - if r == RoleOracle { + switch r { + case RoleOracle: if orc, _ := s.OracleService.Load().(services.Oracle); orc != nil { orc.UpdateOracleNodes(nodeKeys.Copy()) } + case RoleP2PNotary: + if ntr, _ := s.NotaryService.Load().(services.Notary); ntr != nil { + ntr.UpdateNotaryNodes(nodeKeys.Copy()) + } } return nil } diff --git a/pkg/core/notary_test.go b/pkg/core/notary_test.go new file mode 100644 index 000000000..0a5fdc277 --- /dev/null +++ b/pkg/core/notary_test.go @@ -0,0 +1,647 @@ +package core + +import ( + "bytes" + "errors" + "fmt" + "math/rand" + "path" + "sync" + "testing" + "time" + + "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" + "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" + "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "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/network/payload" + "github.com/nspcc-dev/neo-go/pkg/services/notary" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +const notaryModulePath = "../services/notary/" + +func getTestNotary(t *testing.T, bc *Blockchain, walletPath, pass string, onTx func(tx *transaction.Transaction) error) (*wallet.Account, *notary.Notary, *mempool.Pool) { + bc.config.P2PNotary = config.P2PNotary{ + Enabled: true, + UnlockWallet: config.Wallet{ + Path: path.Join(notaryModulePath, walletPath), + Password: pass, + }, + } + mp := mempool.New(10, 1, true) + ntr, err := notary.NewNotary(bc, mp, zaptest.NewLogger(t), onTx) + require.NoError(t, err) + + w, err := wallet.NewWalletFromFile(path.Join(notaryModulePath, walletPath)) + require.NoError(t, err) + require.NoError(t, w.Accounts[0].Decrypt(pass)) + return w.Accounts[0], ntr, mp +} + +func TestNotary(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + var ( + nonce uint32 + nvbDiffFallback uint32 = 20 + ) + + mtx := sync.RWMutex{} + completedTxes := make(map[util.Uint256]*transaction.Transaction) + var unluckies []*payload.P2PNotaryRequest + var ( + finalizeWithError bool + choosy bool + ) + onTransaction := func(tx *transaction.Transaction) error { + mtx.Lock() + defer mtx.Unlock() + if !choosy { + if completedTxes[tx.Hash()] != nil { + panic("transaction was completed twice") + } + if finalizeWithError { + return errors.New("error while finalizing transaction") + } + completedTxes[tx.Hash()] = tx + return nil + } + for _, unl := range unluckies { + if tx.Hash().Equals(unl.FallbackTransaction.Hash()) { + return errors.New("error while finalizing transaction") + } + } + completedTxes[tx.Hash()] = tx + return nil + } + + acc1, ntr1, mp1 := getTestNotary(t, bc, "./testdata/notary1.json", "one", onTransaction) + acc2, _, _ := getTestNotary(t, bc, "./testdata/notary2.json", "two", onTransaction) + randomAcc, err := keys.NewPrivateKey() + require.NoError(t, err) + + bc.SetNotary(ntr1) + bc.RegisterPostBlock(func(bc blockchainer.Blockchainer, pool *mempool.Pool, b *block.Block) { + ntr1.PostPersist(bc, pool, b) + }) + + notaryNodes := keys.PublicKeys{acc1.PrivateKey().PublicKey(), acc2.PrivateKey().PublicKey()} + bc.setNodesByRole(t, true, native.RoleP2PNotary, notaryNodes) + + createFallbackTx := func(requester *wallet.Account, mainTx *transaction.Transaction, nvbIncrement ...uint32) *transaction.Transaction { + fallback := transaction.New(testchain.Network(), []byte{byte(opcode.RET)}, 2000_0000) + fallback.Nonce = nonce + nonce++ + fallback.SystemFee = 1_0000_0000 + fallback.ValidUntilBlock = bc.BlockHeight() + 50 + fallback.Signers = []transaction.Signer{ + { + Account: bc.GetNotaryContractScriptHash(), + Scopes: transaction.None, + }, + { + Account: requester.PrivateKey().PublicKey().GetScriptHash(), + Scopes: transaction.None, + }, + } + nvb := bc.BlockHeight() + nvbDiffFallback + if len(nvbIncrement) != 0 { + nvb += nvbIncrement[0] + } + fallback.Attributes = []transaction.Attribute{ + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: 0}, + }, + { + Type: transaction.NotValidBeforeT, + Value: &transaction.NotValidBefore{Height: nvb}, + }, + { + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{Hash: mainTx.Hash()}, + }, + } + fallback.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...), + VerificationScript: []byte{}, + }, + } + requester.SignTx(fallback) + return fallback + } + + createStandardRequest := func(requesters []*wallet.Account, NVBincrements ...uint32) []*payload.P2PNotaryRequest { + mainTx := *transaction.New(testchain.Network(), []byte{byte(opcode.RET)}, 11000000) + mainTx.Nonce = nonce + nonce++ + mainTx.SystemFee = 100000000 + mainTx.ValidUntilBlock = bc.BlockHeight() + 2*nvbDiffFallback + signers := make([]transaction.Signer, len(requesters)+1) + for i := range requesters { + signers[i] = transaction.Signer{ + Account: requesters[i].PrivateKey().PublicKey().GetScriptHash(), + Scopes: transaction.None, + } + } + signers[len(signers)-1] = transaction.Signer{ + Account: bc.GetNotaryContractScriptHash(), + Scopes: transaction.None, + } + mainTx.Signers = signers + mainTx.Attributes = []transaction.Attribute{ + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: uint8(len(requesters))}, + }, + } + payloads := make([]*payload.P2PNotaryRequest, len(requesters)) + for i := range payloads { + cp := mainTx + main := &cp + scripts := make([]transaction.Witness, len(requesters)+1) + for j := range requesters { + scripts[j].VerificationScript = requesters[j].PrivateKey().PublicKey().GetVerificationScript() + } + scripts[i].InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, requesters[i].PrivateKey().Sign(main.GetSignedPart())...) + main.Scripts = scripts + var fallback *transaction.Transaction + if len(NVBincrements) == len(requesters) { + fallback = createFallbackTx(requesters[i], main, NVBincrements[i]) + } else { + fallback = createFallbackTx(requesters[i], main) + } + payloads[i] = &payload.P2PNotaryRequest{ + MainTransaction: main, + FallbackTransaction: fallback, + Network: netmode.UnitTestNet, + } + } + return payloads + } + createMultisigRequest := func(m int, requesters []*wallet.Account) []*payload.P2PNotaryRequest { + mainTx := *transaction.New(testchain.Network(), []byte{byte(opcode.RET)}, 11000000) + mainTx.Nonce = nonce + nonce++ + mainTx.SystemFee = 100000000 + mainTx.ValidUntilBlock = bc.BlockHeight() + 2*nvbDiffFallback + pubs := make(keys.PublicKeys, len(requesters)) + for i, r := range requesters { + pubs[i] = r.PrivateKey().PublicKey() + } + script, err := smartcontract.CreateMultiSigRedeemScript(m, pubs) + require.NoError(t, err) + mainTx.Signers = []transaction.Signer{ + { + Account: hash.Hash160(script), + Scopes: transaction.None, + }, + { + Account: bc.GetNotaryContractScriptHash(), + Scopes: transaction.None, + }, + } + mainTx.Attributes = []transaction.Attribute{ + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: uint8(len(requesters))}, + }, + } + + payloads := make([]*payload.P2PNotaryRequest, len(requesters)) + // we'll collect only m signatures out of n (so only m payloads are needed), but let's create payloads for all requesters (for the next tests) + for i := range payloads { + cp := mainTx + main := &cp + main.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, requesters[i].PrivateKey().Sign(main.GetSignedPart())...), + VerificationScript: script, + }, + {}, // empty Notary witness + } + fallback := createFallbackTx(requesters[i], main) + payloads[i] = &payload.P2PNotaryRequest{ + MainTransaction: main, + FallbackTransaction: fallback, + Network: netmode.UnitTestNet, + } + } + return payloads + } + checkSigTx := func(t *testing.T, requests []*payload.P2PNotaryRequest, sentCount int, shouldComplete bool) { + nKeys := len(requests) + completedTx := completedTxes[requests[0].MainTransaction.Hash()] + if sentCount == nKeys && shouldComplete { + require.NotNil(t, completedTx, errors.New("main transaction expected to be completed")) + require.Equal(t, nKeys+1, len(completedTx.Signers)) + require.Equal(t, nKeys+1, len(completedTx.Scripts)) + interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) + for i, req := range requests { + require.Equal(t, req.MainTransaction.Scripts[i], completedTx.Scripts[i]) + _, err := bc.verifyHashAgainstScript(completedTx.Signers[i].Account, &completedTx.Scripts[i], interopCtx, -1) + require.NoError(t, err) + } + require.Equal(t, transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().Sign(requests[0].MainTransaction.GetSignedPart())...), + VerificationScript: []byte{}, + }, completedTx.Scripts[nKeys]) + } else { + require.Nil(t, completedTx, fmt.Errorf("main transaction shouldn't be completed: sent %d out of %d requests", sentCount, nKeys)) + } + } + checkMultisigTx := func(t *testing.T, nSigs int, requests []*payload.P2PNotaryRequest, sentCount int, shouldComplete bool) { + completedTx := completedTxes[requests[0].MainTransaction.Hash()] + if sentCount >= nSigs && shouldComplete { + require.NotNil(t, completedTx, errors.New("main transaction expected to be completed")) + require.Equal(t, 2, len(completedTx.Signers)) + require.Equal(t, 2, len(completedTx.Scripts)) + interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) + _, err := bc.verifyHashAgainstScript(completedTx.Signers[0].Account, &completedTx.Scripts[0], interopCtx, -1) + require.NoError(t, err) + require.Equal(t, transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().Sign(requests[0].MainTransaction.GetSignedPart())...), + VerificationScript: []byte{}, + }, completedTx.Scripts[1]) + // check that only nSigs out of nKeys signatures are presented in the invocation script + for i, req := range requests[:nSigs] { + require.True(t, bytes.Contains(completedTx.Scripts[0].InvocationScript, req.MainTransaction.Scripts[0].InvocationScript), fmt.Errorf("signature from extra request #%d shouldn't be presented in the main tx", i)) + } + // the rest (nKeys-nSigs) out of nKeys shouldn't be presented in the invocation script + for i, req := range requests[nSigs:] { + require.False(t, bytes.Contains(completedTx.Scripts[0].InvocationScript, req.MainTransaction.Scripts[0].InvocationScript), fmt.Errorf("signature from extra request #%d shouldn't be presented in the main tx", i)) + } + } else { + require.Nil(t, completedTx, fmt.Errorf("main transaction shouldn't be completed: sent %d out of %d requests", sentCount, nSigs)) + } + } + checkFallbackTxs := func(t *testing.T, requests []*payload.P2PNotaryRequest, shouldComplete bool) { + for i, req := range requests { + completedTx := completedTxes[req.FallbackTransaction.Hash()] + if shouldComplete { + require.NotNil(t, completedTx, fmt.Errorf("fallback transaction for request #%d expected to be completed", i)) + require.Equal(t, 2, len(completedTx.Signers)) + require.Equal(t, 2, len(completedTx.Scripts)) + require.Equal(t, transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc1.PrivateKey().Sign(req.FallbackTransaction.GetSignedPart())...), + VerificationScript: []byte{}, + }, completedTx.Scripts[0]) + interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, nil, completedTx) + _, err := bc.verifyHashAgainstScript(completedTx.Signers[1].Account, &completedTx.Scripts[1], interopCtx, -1) + require.NoError(t, err) + } else { + require.Nil(t, completedTx, fmt.Errorf("fallback transaction for request #%d shouldn't be completed", i)) + } + } + } + checkCompleteStandardRequest := func(t *testing.T, nKeys int, shouldComplete bool, nvbIncrements ...uint32) []*payload.P2PNotaryRequest { + requesters := make([]*wallet.Account, nKeys) + for i := range requesters { + requesters[i], _ = wallet.NewAccount() + } + + requests := createStandardRequest(requesters, nvbIncrements...) + sendOrder := make([]int, nKeys) + for i := range sendOrder { + sendOrder[i] = i + } + rand.Shuffle(nKeys, func(i, j int) { + sendOrder[j], sendOrder[i] = sendOrder[i], sendOrder[j] + }) + for i := range requests { + ntr1.OnNewRequest(requests[sendOrder[i]]) + checkSigTx(t, requests, i+1, shouldComplete) + completedCount := len(completedTxes) + + // check that the same request won't be processed twice + ntr1.OnNewRequest(requests[sendOrder[i]]) + checkSigTx(t, requests, i+1, shouldComplete) + require.Equal(t, completedCount, len(completedTxes)) + } + return requests + } + checkCompleteMultisigRequest := func(t *testing.T, nSigs int, nKeys int, shouldComplete bool) []*payload.P2PNotaryRequest { + requesters := make([]*wallet.Account, nKeys) + for i := range requesters { + requesters[i], _ = wallet.NewAccount() + } + requests := createMultisigRequest(nSigs, requesters) + sendOrder := make([]int, nKeys) + for i := range sendOrder { + sendOrder[i] = i + } + rand.Shuffle(nKeys, func(i, j int) { + sendOrder[j], sendOrder[i] = sendOrder[i], sendOrder[j] + }) + + var submittedRequests []*payload.P2PNotaryRequest + // sent only nSigs (m out of n) requests - it should be enough to complete min tx + for i := 0; i < nSigs; i++ { + submittedRequests = append(submittedRequests, requests[sendOrder[i]]) + + ntr1.OnNewRequest(requests[sendOrder[i]]) + checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete) + + // check that the same request won't be processed twice + ntr1.OnNewRequest(requests[sendOrder[i]]) + checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete) + } + + // sent the rest (n-m) out of n requests: main tx is already collected, so only fallbacks should be applied + completedCount := len(completedTxes) + for i := nSigs; i < nKeys; i++ { + submittedRequests = append(submittedRequests, requests[sendOrder[i]]) + + ntr1.OnNewRequest(requests[sendOrder[i]]) + checkMultisigTx(t, nSigs, submittedRequests, i+1, shouldComplete) + require.Equal(t, completedCount, len(completedTxes)) + } + + return submittedRequests + } + + // OnNewRequest: missing account + ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) + r := checkCompleteStandardRequest(t, 1, false) + checkFallbackTxs(t, r, false) + // set account back for the next tests + ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()}) + + // OnNewRequest: signature request + for _, i := range []int{1, 2, 3, 10} { + r := checkCompleteStandardRequest(t, i, true) + checkFallbackTxs(t, r, false) + } + + // OnNewRequest: multisignature request + r = checkCompleteMultisigRequest(t, 1, 1, true) + checkFallbackTxs(t, r, false) + r = checkCompleteMultisigRequest(t, 1, 2, true) + checkFallbackTxs(t, r, false) + r = checkCompleteMultisigRequest(t, 1, 3, true) + checkFallbackTxs(t, r, false) + r = checkCompleteMultisigRequest(t, 3, 3, true) + checkFallbackTxs(t, r, false) + r = checkCompleteMultisigRequest(t, 3, 4, true) + checkFallbackTxs(t, r, false) + r = checkCompleteMultisigRequest(t, 3, 10, true) + checkFallbackTxs(t, r, false) + + // PostPersist: missing account + finalizeWithError = true + r = checkCompleteStandardRequest(t, 1, false) + checkFallbackTxs(t, r, false) + finalizeWithError = false + ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, r, 1, false) + checkFallbackTxs(t, r, false) + // set account back for the next tests + ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()}) + + // PostPersist: complete main transaction, signature request + finalizeWithError = true + requests := checkCompleteStandardRequest(t, 3, false) + // check PostPersist with finalisation error + finalizeWithError = true + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + // check PostPersist without finalisation error + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), true) + + // PostPersist: complete main transaction, multisignature account + finalizeWithError = true + requests = checkCompleteMultisigRequest(t, 3, 4, false) + checkFallbackTxs(t, requests, false) + // check PostPersist with finalisation error + finalizeWithError = true + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, 3, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + // check PostPersist without finalisation error + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, 3, requests, len(requests), true) + checkFallbackTxs(t, requests, false) + + // PostPersist: complete fallback, signature request + finalizeWithError = true + requests = checkCompleteStandardRequest(t, 3, false) + checkFallbackTxs(t, requests, false) + // make fallbacks valid + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + // check PostPersist for valid fallbacks with finalisation error + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + // check PostPersist for valid fallbacks without finalisation error + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests, true) + + // PostPersist: complete fallback, multisignature request + nSigs, nKeys := 3, 5 + // check OnNewRequest with finalization error + finalizeWithError = true + requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false) + checkFallbackTxs(t, requests, false) + // make fallbacks valid + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + // check PostPersist for valid fallbacks with finalisation error + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, nSigs, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + // check PostPersist for valid fallbacks without finalisation error + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, nSigs, requests, len(requests), false) + checkFallbackTxs(t, requests[:nSigs], true) + // the rest of fallbacks should also be applied even if the main tx was already constructed by the moment they were sent + checkFallbackTxs(t, requests[nSigs:], true) + + // PostPersist: partial fallbacks completion due to finalisation errors + finalizeWithError = true + requests = checkCompleteStandardRequest(t, 5, false) + checkFallbackTxs(t, requests, false) + // make fallbacks valid + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + // some of fallbacks should fail finalisation + unluckies = []*payload.P2PNotaryRequest{requests[0], requests[4]} + lucky := requests[1:4] + choosy = true + // check PostPersist for lucky fallbacks + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, lucky, true) + checkFallbackTxs(t, unluckies, false) + // reset finalisation function for unlucky fallbacks to finalise without an error + choosy = false + finalizeWithError = false + // check PostPersist for unlucky fallbacks + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, lucky, true) + checkFallbackTxs(t, unluckies, true) + + // PostPersist: different NVBs + // check OnNewRequest with finalization error and different NVBs + finalizeWithError = true + requests = checkCompleteStandardRequest(t, 5, false, 1, 2, 3, 4, 5) + checkFallbackTxs(t, requests, false) + // generate blocks to reach the most earlier fallback's NVB + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + // check PostPersist for valid fallbacks without finalisation error + finalizeWithError = false + for i := range requests { + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests[:i+1], true) + checkFallbackTxs(t, requests[i+1:], false) + } + + // OnRequestRemoval: missing account + // check OnNewRequest with finalization error + finalizeWithError = true + requests = checkCompleteStandardRequest(t, 4, false) + checkFallbackTxs(t, requests, false) + // make fallbacks valid and remove one fallback + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + ntr1.UpdateNotaryNodes(keys.PublicKeys{randomAcc.PublicKey()}) + ntr1.OnRequestRemoval(requests[3]) + // non of the fallbacks should be completed + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + // set account back for the next tests + ntr1.UpdateNotaryNodes(keys.PublicKeys{acc1.PrivateKey().PublicKey()}) + + // OnRequestRemoval: signature request, remove one fallback + // check OnNewRequest with finalization error + finalizeWithError = true + requests = checkCompleteStandardRequest(t, 4, false) + checkFallbackTxs(t, requests, false) + // make fallbacks valid and remove one fallback + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + unlucky := requests[3] + ntr1.OnRequestRemoval(unlucky) + // rest of the fallbacks should be completed + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests[:3], true) + require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()]) + + // OnRequestRemoval: signature request, remove all fallbacks + finalizeWithError = true + requests = checkCompleteStandardRequest(t, 4, false) + // remove all fallbacks + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + for i := range requests { + ntr1.OnRequestRemoval(requests[i]) + } + // then the whole request should be removed, i.e. there are no completed transactions + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + + // OnRequestRemoval: signature request, remove unexisting fallback + ntr1.OnRequestRemoval(requests[0]) + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkSigTx(t, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + + // OnRequestRemoval: multisignature request, remove one fallback + nSigs, nKeys = 3, 5 + // check OnNewRequest with finalization error + finalizeWithError = true + requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false) + checkMultisigTx(t, nSigs, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + // make fallbacks valid and remove the last fallback + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + unlucky = requests[nSigs-1] + ntr1.OnRequestRemoval(unlucky) + // then (m-1) out of n fallbacks should be completed + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, nSigs, requests, len(requests), false) + checkFallbackTxs(t, requests[:nSigs-1], true) + require.Nil(t, completedTxes[unlucky.FallbackTransaction.Hash()]) + // the rest (n-(m-1)) out of n fallbacks should also be completed even if main tx has been collected by the moment they were sent + checkFallbackTxs(t, requests[nSigs:], true) + + // OnRequestRemoval: multisignature request, remove all fallbacks + finalizeWithError = true + requests = checkCompleteMultisigRequest(t, nSigs, nKeys, false) + // make fallbacks valid and then remove all of them + _, err = bc.genBlocks(int(nvbDiffFallback)) + require.NoError(t, err) + for i := range requests { + ntr1.OnRequestRemoval(requests[i]) + } + // then the whole request should be removed, i.e. there are no completed transactions + finalizeWithError = false + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, nSigs, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + + // // OnRequestRemoval: multisignature request, remove unexisting fallbac, i.e. there still shouldn't be any completed transactions after this + ntr1.OnRequestRemoval(requests[0]) + require.NoError(t, bc.AddBlock(bc.newBlock())) + checkMultisigTx(t, nSigs, requests, len(requests), false) + checkFallbackTxs(t, requests, false) + + // Subscriptions test + mp1.RunSubscriptions() + go ntr1.Run() + defer func() { + ntr1.Stop() + mp1.StopSubscriptions() + }() + finalizeWithError = false + requester1, _ := wallet.NewAccount() + requester2, _ := wallet.NewAccount() + amount := int64(100_0000_0000) + feer := NewNotaryFeerStub(bc) + transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester1.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50)) + checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(amount)) + transferTokenFromMultisigAccountCheckOK(t, bc, bc.GetNotaryContractScriptHash(), bc.contracts.GAS.Hash, amount, requester2.PrivateKey().PublicKey().GetScriptHash(), int64(bc.BlockHeight()+50)) + checkBalanceOf(t, bc, bc.contracts.Notary.Hash, int(2*amount)) + // create request for 2 standard signatures => main tx should be completed after the second request is added to the pool + requests = createStandardRequest([]*wallet.Account{requester1, requester2}) + require.NoError(t, mp1.Add(requests[0].FallbackTransaction, feer, requests[0])) + require.NoError(t, mp1.Add(requests[1].FallbackTransaction, feer, requests[1])) + require.Eventually(t, func() bool { + mtx.RLock() + defer mtx.RUnlock() + return completedTxes[requests[0].MainTransaction.Hash()] != nil + }, time.Second, time.Millisecond) + checkFallbackTxs(t, requests, false) +} diff --git a/pkg/network/blockqueue_test.go b/pkg/network/blockqueue_test.go index b796cf3f6..cf5a23ed1 100644 --- a/pkg/network/blockqueue_test.go +++ b/pkg/network/blockqueue_test.go @@ -4,13 +4,14 @@ import ( "testing" "time" + "github.com/nspcc-dev/neo-go/internal/fakechain" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/stretchr/testify/assert" "go.uber.org/zap/zaptest" ) func TestBlockQueue(t *testing.T) { - chain := newTestChain() + chain := fakechain.NewFakeChain() // notice, it's not yet running bq := newBlockQueue(0, chain, zaptest.NewLogger(t), nil) blocks := make([]*block.Block, 11) diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 473bb0e58..e084a3252 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -1,333 +1,22 @@ package network import ( - "errors" "fmt" - "math/big" "net" "sync" "sync/atomic" "testing" "time" - "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/internal/fakechain" "github.com/nspcc-dev/neo-go/pkg/config/netmode" - "github.com/nspcc-dev/neo-go/pkg/core/block" - "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/interop" - "github.com/nspcc-dev/neo-go/pkg/core/mempool" - "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/transaction" - "github.com/nspcc-dev/neo-go/pkg/crypto" - "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/capability" "github.com/nspcc-dev/neo-go/pkg/network/payload" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" - "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) -type testChain struct { - config.ProtocolConfiguration - *mempool.Pool - blocksCh []chan<- *block.Block - blockheight uint32 - poolTx func(*transaction.Transaction) error - poolTxWithData func(*transaction.Transaction, interface{}, *mempool.Pool) error - blocks map[util.Uint256]*block.Block - hdrHashes map[uint32]util.Uint256 - txs map[util.Uint256]*transaction.Transaction - verifyWitnessF func() error - maxVerificationGAS int64 - notaryContractScriptHash util.Uint160 - notaryDepositExpiration uint32 - postBlock []func(blockchainer.Blockchainer, *mempool.Pool, *block.Block) - utilityTokenBalance *big.Int -} - -func newTestChain() *testChain { - return &testChain{ - Pool: mempool.New(10, 0), - poolTx: func(*transaction.Transaction) error { return nil }, - poolTxWithData: func(*transaction.Transaction, interface{}, *mempool.Pool) error { return nil }, - blocks: make(map[util.Uint256]*block.Block), - hdrHashes: make(map[uint32]util.Uint256), - txs: make(map[util.Uint256]*transaction.Transaction), - ProtocolConfiguration: config.ProtocolConfiguration{P2PNotaryRequestPayloadPoolSize: 10}, - } -} - -func (chain *testChain) putBlock(b *block.Block) { - chain.blocks[b.Hash()] = b - chain.hdrHashes[b.Index] = b.Hash() - atomic.StoreUint32(&chain.blockheight, b.Index) -} -func (chain *testChain) putHeader(b *block.Block) { - chain.hdrHashes[b.Index] = b.Hash() -} - -func (chain *testChain) putTx(tx *transaction.Transaction) { - chain.txs[tx.Hash()] = tx -} - -func (chain *testChain) ApplyPolicyToTxSet([]*transaction.Transaction) []*transaction.Transaction { - panic("TODO") -} - -func (chain *testChain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool { - panic("TODO") -} -func (*testChain) IsExtensibleAllowed(uint160 util.Uint160) bool { - return true -} - -func (chain *testChain) GetNotaryDepositExpiration(acc util.Uint160) uint32 { - if chain.notaryDepositExpiration != 0 { - return chain.notaryDepositExpiration - } - panic("TODO") -} - -func (chain *testChain) GetNotaryContractScriptHash() util.Uint160 { - if !chain.notaryContractScriptHash.Equals(util.Uint160{}) { - return chain.notaryContractScriptHash - } - panic("TODO") -} - -func (chain *testChain) GetNotaryBalance(acc util.Uint160) *big.Int { - panic("TODO") -} - -func (chain *testChain) GetPolicer() blockchainer.Policer { - return chain -} -func (chain *testChain) GetBaseExecFee() int64 { - return interop.DefaultBaseExecFee -} -func (chain *testChain) GetStoragePrice() int64 { - return native.StoragePrice -} -func (chain *testChain) GetMaxVerificationGAS() int64 { - if chain.maxVerificationGAS != 0 { - return chain.maxVerificationGAS - } - panic("TODO") -} - -func (chain *testChain) PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc blockchainer.Blockchainer, t *transaction.Transaction, data interface{}) error) error { - return chain.poolTxWithData(t, data, mp) -} - -func (chain *testChain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) { - chain.postBlock = append(chain.postBlock, f) -} - -func (chain *testChain) GetConfig() config.ProtocolConfiguration { - return chain.ProtocolConfiguration -} -func (chain *testChain) CalculateClaimable(util.Uint160, uint32) (*big.Int, error) { - panic("TODO") -} - -func (chain *testChain) FeePerByte() int64 { - panic("TODO") -} - -func (chain *testChain) P2PSigExtensionsEnabled() bool { - return true -} - -func (chain *testChain) GetMaxBlockSystemFee() int64 { - panic("TODO") -} - -func (chain *testChain) GetMaxBlockSize() uint32 { - panic("TODO") -} - -func (chain *testChain) AddHeaders(...*block.Header) error { - panic("TODO") -} -func (chain *testChain) AddBlock(block *block.Block) error { - if block.Index == atomic.LoadUint32(&chain.blockheight)+1 { - chain.putBlock(block) - } - return nil -} -func (chain *testChain) AddStateRoot(r *state.MPTRoot) error { - panic("TODO") -} -func (chain *testChain) BlockHeight() uint32 { - return atomic.LoadUint32(&chain.blockheight) -} -func (chain *testChain) Close() { - panic("TODO") -} -func (chain *testChain) HeaderHeight() uint32 { - return atomic.LoadUint32(&chain.blockheight) -} -func (chain *testChain) GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) { - panic("TODO") -} -func (chain *testChain) GetBlock(hash util.Uint256) (*block.Block, error) { - if b, ok := chain.blocks[hash]; ok { - return b, nil - } - return nil, errors.New("not found") -} -func (chain *testChain) GetCommittee() (keys.PublicKeys, error) { - panic("TODO") -} -func (chain *testChain) GetContractState(hash util.Uint160) *state.Contract { - panic("TODO") -} -func (chain *testChain) GetContractScriptHash(id int32) (util.Uint160, error) { - panic("TODO") -} -func (chain *testChain) GetNativeContractScriptHash(name string) (util.Uint160, error) { - panic("TODO") -} -func (chain *testChain) GetHeaderHash(n int) util.Uint256 { - return chain.hdrHashes[uint32(n)] -} -func (chain *testChain) GetHeader(hash util.Uint256) (*block.Header, error) { - b, err := chain.GetBlock(hash) - if err != nil { - return nil, err - } - return b.Header(), nil -} - -func (chain *testChain) GetNextBlockValidators() ([]*keys.PublicKey, error) { - panic("TODO") -} -func (chain *testChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error { - panic("TODO") -} -func (chain *testChain) GetNEP17Balances(util.Uint160) *state.NEP17Balances { - panic("TODO") -} -func (chain *testChain) GetValidators() ([]*keys.PublicKey, error) { - panic("TODO") -} -func (chain *testChain) GetStandByCommittee() keys.PublicKeys { - panic("TODO") -} -func (chain *testChain) GetStandByValidators() keys.PublicKeys { - panic("TODO") -} -func (chain *testChain) GetEnrollments() ([]state.Validator, error) { - panic("TODO") -} -func (chain *testChain) GetStateProof(util.Uint256, []byte) ([][]byte, error) { - panic("TODO") -} -func (chain *testChain) GetStateRoot(height uint32) (*state.MPTRootState, error) { - panic("TODO") -} -func (chain *testChain) GetStorageItem(id int32, key []byte) *state.StorageItem { - panic("TODO") -} -func (chain *testChain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *vm.VM { - panic("TODO") -} -func (chain *testChain) GetStorageItems(id int32) (map[string]*state.StorageItem, error) { - panic("TODO") -} -func (chain *testChain) CurrentHeaderHash() util.Uint256 { - return util.Uint256{} -} -func (chain *testChain) CurrentBlockHash() util.Uint256 { - return util.Uint256{} -} -func (chain *testChain) HasBlock(h util.Uint256) bool { - _, ok := chain.blocks[h] - return ok -} -func (chain *testChain) HasTransaction(h util.Uint256) bool { - _, ok := chain.txs[h] - return ok -} -func (chain *testChain) GetTransaction(h util.Uint256) (*transaction.Transaction, uint32, error) { - if tx, ok := chain.txs[h]; ok { - return tx, 1, nil - } - return nil, 0, errors.New("not found") -} - -func (chain *testChain) GetMemPool() *mempool.Pool { - return chain.Pool -} - -func (chain *testChain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) { - panic("TODO") -} - -func (chain *testChain) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int { - if chain.utilityTokenBalance != nil { - return chain.utilityTokenBalance - } - panic("TODO") -} -func (chain testChain) ManagementContractHash() util.Uint160 { - panic("TODO") -} - -func (chain *testChain) PoolTx(tx *transaction.Transaction, _ ...*mempool.Pool) error { - return chain.poolTx(tx) -} -func (chain testChain) SetOracle(services.Oracle) { - panic("TODO") -} -func (chain *testChain) SubscribeForBlocks(ch chan<- *block.Block) { - chain.blocksCh = append(chain.blocksCh, ch) -} -func (chain *testChain) SubscribeForExecutions(ch chan<- *state.AppExecResult) { - panic("TODO") -} -func (chain *testChain) SubscribeForNotifications(ch chan<- *state.NotificationEvent) { - panic("TODO") -} -func (chain *testChain) SubscribeForTransactions(ch chan<- *transaction.Transaction) { - panic("TODO") -} - -func (chain *testChain) VerifyTx(*transaction.Transaction) error { - panic("TODO") -} -func (chain *testChain) VerifyWitness(util.Uint160, crypto.Verifiable, *transaction.Witness, int64) error { - if chain.verifyWitnessF != nil { - return chain.verifyWitnessF() - } - panic("TODO") -} - -func (chain *testChain) UnsubscribeFromBlocks(ch chan<- *block.Block) { - for i, c := range chain.blocksCh { - if c == ch { - if i < len(chain.blocksCh) { - copy(chain.blocksCh[i:], chain.blocksCh[i+1:]) - } - chain.blocksCh = chain.blocksCh[:len(chain.blocksCh)] - } - } -} -func (chain *testChain) UnsubscribeFromExecutions(ch chan<- *state.AppExecResult) { - panic("TODO") -} -func (chain *testChain) UnsubscribeFromNotifications(ch chan<- *state.NotificationEvent) { - panic("TODO") -} -func (chain *testChain) UnsubscribeFromTransactions(ch chan<- *transaction.Transaction) { - panic("TODO") -} - type testDiscovery struct { sync.Mutex bad []string @@ -500,7 +189,7 @@ func (p *localPeer) CanProcessAddr() bool { } func newTestServer(t *testing.T, serverConfig ServerConfig) *Server { - s, err := newServerFromConstructors(serverConfig, newTestChain(), zaptest.NewLogger(t), + s, err := newServerFromConstructors(serverConfig, fakechain.NewFakeChain(), zaptest.NewLogger(t), newFakeTransp, newFakeConsensus, newTestDiscovery) require.NoError(t, err) return s diff --git a/pkg/network/server.go b/pkg/network/server.go index 7e4a10946..e2be7b021 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -21,6 +21,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/network/capability" "github.com/nspcc-dev/neo-go/pkg/network/extpool" "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/util" "go.uber.org/atomic" @@ -69,7 +70,8 @@ type ( consensus consensus.Service notaryRequestPool *mempool.Pool extensiblePool *extpool.Pool - NotaryFeer NotaryFeer + notaryFeer NotaryFeer + notaryModule *notary.Notary lock sync.RWMutex peers map[Peer]bool @@ -134,13 +136,32 @@ func newServerFromConstructors(config ServerConfig, chain blockchainer.Blockchai transactions: make(chan *transaction.Transaction, 64), } if chain.P2PSigExtensionsEnabled() { - s.NotaryFeer = NewNotaryFeer(chain) - s.notaryRequestPool = mempool.New(chain.GetConfig().P2PNotaryRequestPayloadPoolSize, 1) + s.notaryFeer = NewNotaryFeer(chain) + s.notaryRequestPool = mempool.New(chain.GetConfig().P2PNotaryRequestPayloadPoolSize, 1, chain.GetConfig().P2PNotary.Enabled) chain.RegisterPostBlock(func(bc blockchainer.Blockchainer, txpool *mempool.Pool, _ *block.Block) { s.notaryRequestPool.RemoveStale(func(t *transaction.Transaction) bool { return bc.IsTxStillRelevant(t, txpool, true) - }, s.NotaryFeer) + }, s.notaryFeer) }) + if chain.GetConfig().P2PNotary.Enabled { + n, err := notary.NewNotary(chain, s.notaryRequestPool, s.log, func(tx *transaction.Transaction) error { + r := s.RelayTxn(tx) + if r != RelaySucceed { + return fmt.Errorf("can't pool notary tx: hash %s, reason: %d", tx.Hash().StringLE(), byte(r)) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to create Notary module: %w", err) + } + s.notaryModule = n + chain.SetNotary(n) + chain.RegisterPostBlock(func(bc blockchainer.Blockchainer, pool *mempool.Pool, b *block.Block) { + s.notaryModule.PostPersist(bc, pool, b) + }) + } + } else if chain.GetConfig().P2PNotary.Enabled { + return nil, errors.New("P2PSigExtensions are disabled, but Notary service is enable") } s.bQueue = newBlockQueue(maxBlockBatch, chain, log, func(b *block.Block) { if !s.consensusStarted.Load() { @@ -235,6 +256,10 @@ func (s *Server) Start(errChan chan error) { if s.oracle != nil { go s.oracle.Run() } + if s.notaryModule != nil { + s.notaryRequestPool.RunSubscriptions() + go s.notaryModule.Run() + } go s.relayBlocksLoop() go s.bQueue.run() go s.transport.Accept() @@ -257,6 +282,10 @@ func (s *Server) Shutdown() { if s.oracle != nil { s.oracle.Shutdown() } + if s.notaryModule != nil { + s.notaryModule.Stop() + s.notaryRequestPool.StopSubscriptions() + } close(s.quit) } @@ -805,7 +834,7 @@ func (s *Server) handleP2PNotaryRequestCmd(r *payload.P2PNotaryRequest) error { // verifyAndPoolNotaryRequest verifies NotaryRequest payload and adds it to the payload mempool. func (s *Server) verifyAndPoolNotaryRequest(r *payload.P2PNotaryRequest) RelayReason { - if err := s.chain.PoolTxWithData(r.FallbackTransaction, r, s.notaryRequestPool, s.NotaryFeer, verifyNotaryRequest); err != nil { + if err := s.chain.PoolTxWithData(r.FallbackTransaction, r, s.notaryRequestPool, s.notaryFeer, verifyNotaryRequest); err != nil { switch { case errors.Is(err, core.ErrAlreadyExists): return RelayAlreadyExists @@ -827,7 +856,8 @@ func verifyNotaryRequest(bc blockchainer.Blockchainer, _ *transaction.Transactio if err := bc.VerifyWitness(payer, r, &r.Witness, bc.GetPolicer().GetMaxVerificationGAS()); err != nil { return fmt.Errorf("bad P2PNotaryRequest payload witness: %w", err) } - if r.FallbackTransaction.Sender() != bc.GetNotaryContractScriptHash() { + notaryHash := bc.GetNotaryContractScriptHash() + if r.FallbackTransaction.Sender() != notaryHash { return errors.New("P2PNotary contract should be a sender of the fallback transaction") } depositExpiration := bc.GetNotaryDepositExpiration(payer) diff --git a/pkg/network/server_test.go b/pkg/network/server_test.go index 04cf91ca7..ef3d6432f 100644 --- a/pkg/network/server_test.go +++ b/pkg/network/server_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/nspcc-dev/neo-go/internal/fakechain" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config/netmode" @@ -47,7 +48,7 @@ func (f *fakeConsensus) OnTransaction(tx *transaction.Transaction) { f.txs = func (f *fakeConsensus) GetPayload(h util.Uint256) *payload.Extensible { panic("implement me") } func TestNewServer(t *testing.T) { - bc := &testChain{} + bc := &fakechain.FakeChain{} s, err := newServerFromConstructors(ServerConfig{}, bc, nil, newFakeTransp, newFakeConsensus, newTestDiscovery) require.Error(t, err) @@ -223,7 +224,7 @@ func TestGetBlocksByIndex(t *testing.T) { checkPingRespond(t, 3, 5000, 1+3*payload.MaxHashesCount) // Receive some blocks. - s.chain.(*testChain).blockheight = 2123 + s.chain.(*fakechain.FakeChain).Blockheight = 2123 // Minimum chunk has priority. checkPingRespond(t, 5, 5000, 2124) @@ -392,7 +393,7 @@ func TestBlock(t *testing.T) { s, shutdown := startTestServer(t) defer shutdown() - atomic2.StoreUint32(&s.chain.(*testChain).blockheight, 12344) + atomic2.StoreUint32(&s.chain.(*fakechain.FakeChain).Blockheight, 12344) require.Equal(t, uint32(12344), s.chain.BlockHeight()) b := block.New(netmode.UnitTestNet, false) @@ -405,7 +406,7 @@ func TestConsensus(t *testing.T) { s, shutdown := startTestServer(t) defer shutdown() - atomic2.StoreUint32(&s.chain.(*testChain).blockheight, 4) + atomic2.StoreUint32(&s.chain.(*fakechain.FakeChain).Blockheight, 4) p := newLocalPeer(t, s) p.handshaked = true @@ -417,11 +418,11 @@ func TestConsensus(t *testing.T) { return NewMessage(CMDExtensible, pl) } - s.chain.(*testChain).verifyWitnessF = func() error { return errors.New("invalid") } + s.chain.(*fakechain.FakeChain).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 } + s.chain.(*fakechain.FakeChain).VerifyWitnessF = func() error { return nil } require.NoError(t, s.handleMessage(p, msg)) require.Contains(t, s.consensus.(*fakeConsensus).payloads, msg.Payload.(*payload.Extensible)) @@ -471,7 +472,7 @@ func TestTransaction(t *testing.T) { }) t.Run("bad", func(t *testing.T) { tx := newDummyTx() - s.chain.(*testChain).poolTx = func(*transaction.Transaction) error { return core.ErrInsufficientFunds } + s.chain.(*fakechain.FakeChain).PoolTxF = func(*transaction.Transaction) error { return core.ErrInsufficientFunds } s.testHandleMessage(t, nil, CMDTX, tx) for _, ftx := range s.consensus.(*fakeConsensus).txs { require.NotEqual(t, ftx, tx) @@ -505,19 +506,19 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType, func TestGetData(t *testing.T) { s, shutdown := startTestServer(t) defer shutdown() - s.chain.(*testChain).utilityTokenBalance = big.NewInt(1000000) + s.chain.(*fakechain.FakeChain).UtilityTokenBalance = big.NewInt(1000000) t.Run("block", func(t *testing.T) { b := newDummyBlock(2, 0) hs := []util.Uint256{random.Uint256(), b.Hash(), random.Uint256()} - s.chain.(*testChain).putBlock(b) + s.chain.(*fakechain.FakeChain).PutBlock(b) notFound := []util.Uint256{hs[0], hs[2]} s.testHandleGetData(t, payload.BlockType, hs, notFound, b) }) t.Run("transaction", func(t *testing.T) { tx := newDummyTx() hs := []util.Uint256{random.Uint256(), tx.Hash(), random.Uint256()} - s.chain.(*testChain).putTx(tx) + s.chain.(*fakechain.FakeChain).PutTx(tx) notFound := []util.Uint256{hs[0], hs[2]} s.testHandleGetData(t, payload.TXType, hs, notFound, tx) }) @@ -567,7 +568,7 @@ func initGetBlocksTest(t *testing.T) (*Server, func(), []*block.Block) { var blocks []*block.Block for i := uint32(12); i <= 15; i++ { b := newDummyBlock(i, 3) - s.chain.(*testChain).putBlock(b) + s.chain.(*fakechain.FakeChain).PutBlock(b) blocks = append(blocks, b) } return s, shutdown, blocks @@ -633,7 +634,7 @@ func TestGetBlockByIndex(t *testing.T) { s.testHandleMessage(t, p, CMDGetBlockByIndex, &payload.GetBlockByIndex{IndexStart: blocks[0].Index, Count: -1}) }) t.Run("-1, last header", func(t *testing.T) { - s.chain.(*testChain).putHeader(newDummyBlock(16, 2)) + s.chain.(*fakechain.FakeChain).PutHeader(newDummyBlock(16, 2)) actual = nil expected = blocks s.testHandleMessage(t, p, CMDGetBlockByIndex, &payload.GetBlockByIndex{IndexStart: blocks[0].Index, Count: -1}) @@ -683,7 +684,7 @@ func TestGetHeaders(t *testing.T) { func TestInv(t *testing.T) { s, shutdown := startTestServer(t) defer shutdown() - s.chain.(*testChain).utilityTokenBalance = big.NewInt(10000000) + s.chain.(*fakechain.FakeChain).UtilityTokenBalance = big.NewInt(10000000) var actual []util.Uint256 p := newLocalPeer(t, s) @@ -696,7 +697,7 @@ func TestInv(t *testing.T) { t.Run("blocks", func(t *testing.T) { b := newDummyBlock(10, 3) - s.chain.(*testChain).putBlock(b) + s.chain.(*fakechain.FakeChain).PutBlock(b) hs := []util.Uint256{random.Uint256(), b.Hash(), random.Uint256()} s.testHandleMessage(t, p, CMDInv, &payload.Inventory{ Type: payload.BlockType, @@ -706,7 +707,7 @@ func TestInv(t *testing.T) { }) t.Run("transaction", func(t *testing.T) { tx := newDummyTx() - s.chain.(*testChain).putTx(tx) + s.chain.(*fakechain.FakeChain).PutTx(tx) hs := []util.Uint256{random.Uint256(), tx.Hash(), random.Uint256()} s.testHandleMessage(t, p, CMDInv, &payload.Inventory{ Type: payload.TXType, @@ -716,8 +717,8 @@ func TestInv(t *testing.T) { }) t.Run("extensible", func(t *testing.T) { ep := payload.NewExtensible(netmode.UnitTestNet) - s.chain.(*testChain).verifyWitnessF = func() error { return nil } - ep.ValidBlockEnd = s.chain.(*testChain).BlockHeight() + 1 + s.chain.(*fakechain.FakeChain).VerifyWitnessF = func() error { return nil } + ep.ValidBlockEnd = s.chain.(*fakechain.FakeChain).BlockHeight() + 1 ok, err := s.extensiblePool.Add(ep) require.NoError(t, err) require.True(t, ok) @@ -865,7 +866,7 @@ func TestMemPool(t *testing.T) { } } - bc := s.chain.(*testChain) + bc := s.chain.(*fakechain.FakeChain) expected := make([]util.Uint256, 4) for i := range expected { tx := newDummyTx() @@ -878,27 +879,27 @@ func TestMemPool(t *testing.T) { } func TestVerifyNotaryRequest(t *testing.T) { - bc := newTestChain() - bc.maxVerificationGAS = 10 - bc.notaryContractScriptHash = util.Uint160{1, 2, 3} + bc := fakechain.NewFakeChain() + bc.MaxVerificationGAS = 10 + bc.NotaryContractScriptHash = util.Uint160{1, 2, 3} newNotaryRequest := func() *payload.P2PNotaryRequest { return &payload.P2PNotaryRequest{ MainTransaction: &transaction.Transaction{Script: []byte{0, 1, 2}}, FallbackTransaction: &transaction.Transaction{ ValidUntilBlock: 321, - Signers: []transaction.Signer{{Account: bc.notaryContractScriptHash}, {Account: random.Uint160()}}, + Signers: []transaction.Signer{{Account: bc.NotaryContractScriptHash}, {Account: random.Uint160()}}, }, Witness: transaction.Witness{}, } } t.Run("bad payload witness", func(t *testing.T) { - bc.verifyWitnessF = func() error { return errors.New("bad witness") } + bc.VerifyWitnessF = func() error { return errors.New("bad witness") } require.Error(t, verifyNotaryRequest(bc, nil, newNotaryRequest())) }) t.Run("bad fallback sender", func(t *testing.T) { - bc.verifyWitnessF = func() error { return nil } + bc.VerifyWitnessF = func() error { return nil } r := newNotaryRequest() r.FallbackTransaction.Signers[0] = transaction.Signer{Account: util.Uint160{7, 8, 9}} require.Error(t, verifyNotaryRequest(bc, nil, r)) @@ -906,13 +907,13 @@ func TestVerifyNotaryRequest(t *testing.T) { t.Run("expired deposit", func(t *testing.T) { r := newNotaryRequest() - bc.notaryDepositExpiration = r.FallbackTransaction.ValidUntilBlock + bc.NotaryDepositExpiration = r.FallbackTransaction.ValidUntilBlock require.Error(t, verifyNotaryRequest(bc, nil, r)) }) t.Run("good", func(t *testing.T) { r := newNotaryRequest() - bc.notaryDepositExpiration = r.FallbackTransaction.ValidUntilBlock + 1 + bc.NotaryDepositExpiration = r.FallbackTransaction.ValidUntilBlock + 1 require.NoError(t, verifyNotaryRequest(bc, nil, r)) }) } diff --git a/pkg/services/notary/node.go b/pkg/services/notary/node.go new file mode 100644 index 000000000..15df2a5e1 --- /dev/null +++ b/pkg/services/notary/node.go @@ -0,0 +1,54 @@ +package notary + +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/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +// UpdateNotaryNodes implements Notary interface and updates current notary account. +func (n *Notary) UpdateNotaryNodes(notaryNodes keys.PublicKeys) { + n.accMtx.Lock() + defer n.accMtx.Unlock() + + if n.currAccount != nil { + for _, node := range notaryNodes { + if node.Equal(n.currAccount.PrivateKey().PublicKey()) { + return + } + } + } + + var acc *wallet.Account + for _, node := range notaryNodes { + acc = n.wallet.GetAccount(node.GetScriptHash()) + if acc != nil { + if acc.PrivateKey() != nil { + break + } + err := acc.Decrypt(n.Config.MainCfg.UnlockWallet.Password) + if err != nil { + n.Config.Log.Warn("can't unlock notary node account", + zap.String("address", address.Uint160ToString(acc.Contract.ScriptHash())), + zap.Error(err)) + acc = nil + } + break + } + } + + n.currAccount = acc + if acc == nil { + n.reqMtx.Lock() + n.requests = make(map[util.Uint256]*request) + n.reqMtx.Unlock() + } +} + +func (n *Notary) getAccount() *wallet.Account { + n.accMtx.RLock() + defer n.accMtx.RUnlock() + return n.currAccount +} diff --git a/pkg/services/notary/node_test.go b/pkg/services/notary/node_test.go new file mode 100644 index 000000000..6c0686b88 --- /dev/null +++ b/pkg/services/notary/node_test.go @@ -0,0 +1,71 @@ +package notary + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/fakechain" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" + "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func getTestNotary(t *testing.T, bc blockchainer.Blockchainer, walletPath, pass string) (*wallet.Account, *Notary, *mempool.Pool) { + bc.(*fakechain.FakeChain).ProtocolConfiguration.P2PNotary = config.P2PNotary{ + Enabled: true, + UnlockWallet: config.Wallet{ + Path: walletPath, + Password: pass, + }, + } + mp := mempool.New(10, 1, true) + ntr, err := NewNotary(bc, mp, zaptest.NewLogger(t), nil) + require.NoError(t, err) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + require.NoError(t, w.Accounts[0].Decrypt(pass)) + return w.Accounts[0], ntr, mp +} + +func TestUpdateNotaryNodes(t *testing.T) { + bc := fakechain.NewFakeChain() + acc, ntr, _ := getTestNotary(t, bc, "./testdata/notary1.json", "one") + randomKey, err := keys.NewPrivateKey() + require.NoError(t, err) + // currAcc is nil before UpdateNotaryNodes call + require.Nil(t, ntr.currAccount) + // set account for the first time + ntr.UpdateNotaryNodes(keys.PublicKeys{acc.PrivateKey().PublicKey()}) + require.Equal(t, acc, ntr.currAccount) + + t.Run("account is already set", func(t *testing.T) { + ntr.UpdateNotaryNodes(keys.PublicKeys{acc.PrivateKey().PublicKey(), randomKey.PublicKey()}) + require.Equal(t, acc, ntr.currAccount) + }) + + t.Run("another account from the same wallet", func(t *testing.T) { + t.Run("good config password", func(t *testing.T) { + w, err := wallet.NewWalletFromFile("./testdata/notary1.json") + require.NoError(t, err) + require.NoError(t, w.Accounts[1].Decrypt("one")) + ntr.UpdateNotaryNodes(keys.PublicKeys{w.Accounts[1].PrivateKey().PublicKey()}) + require.Equal(t, w.Accounts[1], ntr.currAccount) + }) + t.Run("bad config password", func(t *testing.T) { + w, err := wallet.NewWalletFromFile("./testdata/notary1.json") + require.NoError(t, err) + require.NoError(t, w.Accounts[2].Decrypt("four")) + ntr.UpdateNotaryNodes(keys.PublicKeys{w.Accounts[2].PrivateKey().PublicKey()}) + require.Nil(t, ntr.currAccount) + }) + }) + + t.Run("unknown account", func(t *testing.T) { + ntr.UpdateNotaryNodes(keys.PublicKeys{randomKey.PublicKey()}) + require.Nil(t, ntr.currAccount) + }) +} diff --git a/pkg/services/notary/notary.go b/pkg/services/notary/notary.go new file mode 100644 index 000000000..5b4c51f3d --- /dev/null +++ b/pkg/services/notary/notary.go @@ -0,0 +1,394 @@ +package notary + +import ( + "bytes" + "crypto/elliptic" + "encoding/hex" + "errors" + "fmt" + "sync" + + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" + "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "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/network/payload" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "go.uber.org/zap" +) + +type ( + // Notary represents Notary module. + Notary struct { + Config Config + + // onTransaction is a callback for completed transactions (mains or fallbacks) sending. + onTransaction func(tx *transaction.Transaction) error + + // reqMtx protects requests list. + reqMtx sync.RWMutex + // requests represents the map of main transactions which needs to be completed + // with the associated fallback transactions grouped by the main transaction hash + requests map[util.Uint256]*request + + // accMtx protects account. + accMtx sync.RWMutex + currAccount *wallet.Account + wallet *wallet.Wallet + + mp *mempool.Pool + // requests channel + reqCh chan mempool.Event + stopCh chan struct{} + } + + // Config represents external configuration for Notary module. + Config struct { + MainCfg config.P2PNotary + Chain blockchainer.Blockchainer + Log *zap.Logger + } +) + +// request represents Notary service request. +type request struct { + typ RequestType + // isSent indicates whether main transaction was successfully sent to the network. + isSent bool + main *transaction.Transaction + // minNotValidBefore is the minimum NVB value among fallbacks transactions. + // We stop trying to send mainTx to the network if the chain reaches minNotValidBefore height. + minNotValidBefore uint32 + fallbacks []*transaction.Transaction + // nSigs is the number of signatures to be collected. + // nSigs == nKeys for standard signature request; + // nSigs <= nKeys for multisignature request. + // nSigs is 0 when all received requests were invalid, so check request.typ before access to nSigs. + nSigs uint8 + // nSigsCollected is the number of already collected signatures + nSigsCollected uint8 + + // sigs is a map of partial multisig invocation scripts [opcode.PUSHDATA1+64+signatureBytes] grouped by public keys + sigs map[*keys.PublicKey][]byte +} + +// NewNotary returns new Notary module. +func NewNotary(bc blockchainer.Blockchainer, mp *mempool.Pool, log *zap.Logger, onTransaction func(tx *transaction.Transaction) error) (*Notary, error) { + cfg := bc.GetConfig().P2PNotary + w := cfg.UnlockWallet + wallet, err := wallet.NewWalletFromFile(w.Path) + if err != nil { + return nil, err + } + + haveAccount := false + for _, acc := range wallet.Accounts { + if err := acc.Decrypt(w.Password); err == nil { + haveAccount = true + break + } + } + if !haveAccount { + return nil, errors.New("no wallet account could be unlocked") + } + + return &Notary{ + requests: make(map[util.Uint256]*request), + Config: Config{ + MainCfg: cfg, + Chain: bc, + Log: log, + }, + wallet: wallet, + onTransaction: onTransaction, + mp: mp, + reqCh: make(chan mempool.Event), + stopCh: make(chan struct{}), + }, nil +} + +// Run runs Notary module and should be called in a separate goroutine. +func (n *Notary) Run() { + n.mp.SubscribeForTransactions(n.reqCh) + for { + select { + case <-n.stopCh: + n.mp.UnsubscribeFromTransactions(n.reqCh) + return + case event := <-n.reqCh: + if req, ok := event.Data.(*payload.P2PNotaryRequest); ok { + switch event.Type { + case mempool.TransactionAdded: + n.OnNewRequest(req) + case mempool.TransactionRemoved: + n.OnRequestRemoval(req) + } + } + } + } +} + +// Stop shutdowns Notary module. +func (n *Notary) Stop() { + close(n.stopCh) +} + +// OnNewRequest is a callback method which is called after new notary request is added to the notary request pool. +func (n *Notary) OnNewRequest(payload *payload.P2PNotaryRequest) { + if n.getAccount() == nil { + return + } + + nvbFallback := payload.FallbackTransaction.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height + nKeys := payload.MainTransaction.GetAttributes(transaction.NotaryAssistedT)[0].Value.(*transaction.NotaryAssisted).NKeys + typ, nSigs, pubs, validationErr := n.verifyIncompleteWitnesses(payload.MainTransaction, nKeys) + n.reqMtx.Lock() + defer n.reqMtx.Unlock() + r, exists := n.requests[payload.MainTransaction.Hash()] + if exists { + for _, fb := range r.fallbacks { + if fb.Hash().Equals(payload.FallbackTransaction.Hash()) { + return // then we already have processed this request + } + } + if nvbFallback < r.minNotValidBefore { + r.minNotValidBefore = nvbFallback + } + if r.typ == Unknown && validationErr == nil { + r.typ = typ + r.nSigs = nSigs + } + } else { + r = &request{ + nSigs: nSigs, + main: payload.MainTransaction, + typ: typ, + minNotValidBefore: nvbFallback, + } + n.requests[payload.MainTransaction.Hash()] = r + } + r.fallbacks = append(r.fallbacks, payload.FallbackTransaction) + if exists && r.typ != Unknown && r.nSigsCollected >= r.nSigs { // already collected sufficient number of signatures to complete main transaction + return + } + if validationErr == nil { + loop: + for i, w := range payload.MainTransaction.Scripts { + if payload.MainTransaction.Signers[i].Account.Equals(n.Config.Chain.GetNotaryContractScriptHash()) { + continue + } + if len(w.InvocationScript) != 0 && len(w.VerificationScript) != 0 { + switch r.typ { + case Signature: + if !exists { + r.nSigsCollected++ + } else if len(r.main.Scripts[i].InvocationScript) == 0 { // need this check because signature can already be added (consider receiving the same payload multiple times) + r.main.Scripts[i] = w + r.nSigsCollected++ + } + if r.nSigsCollected == r.nSigs { + break loop + } + case MultiSignature: + if r.sigs == nil { + r.sigs = make(map[*keys.PublicKey][]byte) + } + + hash := r.main.GetSignedHash().BytesBE() + for _, pub := range pubs { + if r.sigs[pub] != nil { + continue // signature for this pub has already been added + } + if pub.Verify(w.InvocationScript[2:], hash) { // then pub is the owner of the signature + r.sigs[pub] = w.InvocationScript + r.nSigsCollected++ + if r.nSigsCollected == r.nSigs { + var invScript []byte + for j := range pubs { + if sig, ok := r.sigs[pubs[j]]; ok { + invScript = append(invScript, sig...) + } + } + r.main.Scripts[i].InvocationScript = invScript + } + break loop + } + } + // pubKey was not found for the signature i.e. signature is bad - we're OK with that, let the fallback TX to be added + break loop // only one multisignature is allowed + } + } + } + } + if r.typ != Unknown && r.nSigsCollected == nSigs && r.minNotValidBefore > n.Config.Chain.BlockHeight() { + if err := n.finalize(r.main); err != nil { + n.Config.Log.Error("failed to finalize main transaction", zap.Error(err)) + } else { + r.isSent = true + } + } +} + +// OnRequestRemoval is a callback which is called after fallback transaction is removed +// from the notary payload pool due to expiration, main tx appliance or any other reason. +func (n *Notary) OnRequestRemoval(pld *payload.P2PNotaryRequest) { + if n.getAccount() == nil { + return + } + + n.reqMtx.Lock() + defer n.reqMtx.Unlock() + r, ok := n.requests[pld.MainTransaction.Hash()] + if !ok { + return + } + for i, fb := range r.fallbacks { + if fb.Hash().Equals(pld.FallbackTransaction.Hash()) { + r.fallbacks = append(r.fallbacks[:i], r.fallbacks[i+1:]...) + break + } + } + if len(r.fallbacks) == 0 { + delete(n.requests, r.main.Hash()) + } +} + +// PostPersist is a callback which is called after new block is persisted. +func (n *Notary) PostPersist(bc blockchainer.Blockchainer, pool *mempool.Pool, b *block.Block) { + if n.getAccount() == nil { + return + } + + n.reqMtx.Lock() + defer n.reqMtx.Unlock() + for h, r := range n.requests { + if !r.isSent && r.typ != Unknown && r.nSigs == r.nSigsCollected && r.minNotValidBefore > bc.BlockHeight() { + if err := n.finalize(r.main); err != nil { + n.Config.Log.Error("failed to finalize main transaction", zap.Error(err)) + } else { + r.isSent = true + } + continue + } + if r.minNotValidBefore <= bc.BlockHeight() { // then at least one of the fallbacks can already be sent. + newFallbacks := r.fallbacks[:0] + for _, fb := range r.fallbacks { + if nvb := fb.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height; nvb <= bc.BlockHeight() { + if err := n.finalize(fb); err != nil { + newFallbacks = append(newFallbacks, fb) // wait for the next block to resend them + } + } else { + newFallbacks = append(newFallbacks, fb) + } + } + if len(newFallbacks) == 0 { + delete(n.requests, h) + } else { + r.fallbacks = newFallbacks + } + } + } +} + +// finalize adds missing Notary witnesses to the transaction (main or fallback) and pushes it to the network. +func (n *Notary) finalize(tx *transaction.Transaction) error { + acc := n.getAccount() + if acc == nil { + panic(errors.New("no available Notary account")) // unreachable code, because all callers of `finalize` check that acc != nil + } + notaryWitness := transaction.Witness{ + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, acc.PrivateKey().Sign(tx.GetSignedPart())...), + VerificationScript: []byte{}, + } + for i, signer := range tx.Signers { + if signer.Account == n.Config.Chain.GetNotaryContractScriptHash() { + tx.Scripts[i] = notaryWitness + break + } + } + return n.onTransaction(tx) +} + +// verifyIncompleteWitnesses checks that tx either doesn't have all witnesses attached (in this case none of them +// can be multisignature), or it only has a partial multisignature. It returns the request type (sig/multisig), the +// number of signatures to be collected, sorted public keys (for multisig request only) and an error. +func (n *Notary) verifyIncompleteWitnesses(tx *transaction.Transaction, nKeys uint8) (RequestType, uint8, keys.PublicKeys, error) { + var ( + typ RequestType + nSigs int + nKeysActual uint8 + pubsBytes [][]byte + pubs keys.PublicKeys + ok bool + ) + if len(tx.Signers) < 2 { + return Unknown, 0, nil, errors.New("transaction should have at least 2 signers") + } + if len(tx.Signers) != len(tx.Scripts) { + return Unknown, 0, nil, fmt.Errorf("transaction should have %d witnesses attached (completed + dummy)", len(tx.Signers)) + } + if !tx.HasSigner(n.Config.Chain.GetNotaryContractScriptHash()) { + return Unknown, 0, nil, fmt.Errorf("P2PNotary contract should be a signer of the transaction") + } + + for i, w := range tx.Scripts { + // do not check witness for Notary contract -- it will be replaced by proper witness in any case. + if tx.Signers[i].Account == n.Config.Chain.GetNotaryContractScriptHash() { + continue + } + if len(w.VerificationScript) == 0 { + // then it's a contract verification (can be combined with anything) + continue + } + if !tx.Signers[i].Account.Equals(hash.Hash160(w.VerificationScript)) { // https://github.com/nspcc-dev/neo-go/pull/1658#discussion_r564265987 + return Unknown, 0, nil, fmt.Errorf("transaction should have valid verification script for signer #%d", i) + } + if nSigs, pubsBytes, ok = vm.ParseMultiSigContract(w.VerificationScript); ok { + if typ == Signature || typ == MultiSignature { + return Unknown, 0, nil, fmt.Errorf("bad type of witness #%d: only one multisignature witness is allowed", i) + } + typ = MultiSignature + nKeysActual = uint8(len(pubsBytes)) + if len(w.InvocationScript) != 66 || !bytes.HasPrefix(w.InvocationScript, []byte{byte(opcode.PUSHDATA1), 64}) { + return Unknown, 0, nil, fmt.Errorf("multisignature invocation script should have length = 66 and be of the form [PUSHDATA1, 64, signatureBytes...]") + } + continue + } + if vm.IsSignatureContract(w.VerificationScript) { + if typ == MultiSignature { + return Unknown, 0, nil, fmt.Errorf("bad type of witness #%d: multisignature witness can not be combined with other witnesses", i) + } + typ = Signature + nSigs = int(nKeys) + continue + } + return Unknown, 0, nil, fmt.Errorf("unable to define the type of witness #%d", i) + } + switch typ { + case Signature: + if len(tx.Scripts) < int(nKeys+1) { + return Unknown, 0, nil, fmt.Errorf("transaction should comtain at least %d witnesses (1 for notary + nKeys)", nKeys+1) + } + case MultiSignature: + if nKeysActual != nKeys { + return Unknown, 0, nil, fmt.Errorf("bad m out of n partial multisignature witness: expected n = %d, got n = %d", nKeys, nKeysActual) + } + pubs = make(keys.PublicKeys, len(pubsBytes)) + for i, pBytes := range pubsBytes { + pub, err := keys.NewPublicKeyFromBytes(pBytes, elliptic.P256()) + if err != nil { + return Unknown, 0, nil, fmt.Errorf("invalid bytes of #%d public key: %s", i, hex.EncodeToString(pBytes)) + } + pubs[i] = pub + } + default: + return Unknown, 0, nil, errors.New("unexpected Notary request type") + } + return typ, uint8(nSigs), pubs, nil +} diff --git a/pkg/services/notary/notary_test.go b/pkg/services/notary/notary_test.go new file mode 100644 index 000000000..452f6a173 --- /dev/null +++ b/pkg/services/notary/notary_test.go @@ -0,0 +1,427 @@ +package notary + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/fakechain" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "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/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestWallet(t *testing.T) { + bc := fakechain.NewFakeChain() + + t.Run("unexisting wallet", func(t *testing.T) { + bc.ProtocolConfiguration.P2PNotary = config.P2PNotary{ + Enabled: true, + UnlockWallet: config.Wallet{ + Path: "./testdata/does_not_exists.json", + Password: "one", + }, + } + _, err := NewNotary(bc, mempool.New(1, 1, true), zaptest.NewLogger(t), nil) + require.Error(t, err) + }) + + t.Run("bad password", func(t *testing.T) { + bc.ProtocolConfiguration.P2PNotary = config.P2PNotary{ + Enabled: true, + UnlockWallet: config.Wallet{ + Path: "./testdata/notary1.json", + Password: "invalid", + }, + } + _, err := NewNotary(bc, mempool.New(1, 1, true), zaptest.NewLogger(t), nil) + require.Error(t, err) + }) + + t.Run("good", func(t *testing.T) { + bc.ProtocolConfiguration.P2PNotary = config.P2PNotary{ + Enabled: true, + UnlockWallet: config.Wallet{ + Path: "./testdata/notary1.json", + Password: "one", + }, + } + _, err := NewNotary(bc, mempool.New(1, 1, true), zaptest.NewLogger(t), nil) + require.NoError(t, err) + }) +} + +func TestVerifyIncompleteRequest(t *testing.T) { + bc := fakechain.NewFakeChain() + notaryContractHash := util.Uint160{1, 2, 3} + bc.NotaryContractScriptHash = notaryContractHash + _, ntr, _ := getTestNotary(t, bc, "./testdata/notary1.json", "one") + sig := append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64)...) // we're not interested in signature correctness + acc1, _ := keys.NewPrivateKey() + acc2, _ := keys.NewPrivateKey() + acc3, _ := keys.NewPrivateKey() + sigScript1 := acc1.PublicKey().GetVerificationScript() + sigScript2 := acc2.PublicKey().GetVerificationScript() + sigScript3 := acc3.PublicKey().GetVerificationScript() + multisigScript1, err := smartcontract.CreateMultiSigRedeemScript(1, keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}) + require.NoError(t, err) + multisigScriptHash1 := hash.Hash160(multisigScript1) + multisigScript2, err := smartcontract.CreateMultiSigRedeemScript(2, keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}) + require.NoError(t, err) + multisigScriptHash2 := hash.Hash160(multisigScript2) + + checkErr := func(t *testing.T, tx *transaction.Transaction, nKeys uint8) { + typ, nSigs, pubs, err := ntr.verifyIncompleteWitnesses(tx, nKeys) + require.Error(t, err) + require.Equal(t, Unknown, typ) + require.Equal(t, uint8(0), nSigs) + require.Nil(t, pubs) + } + + errCases := map[string]struct { + tx *transaction.Transaction + nKeys uint8 + }{ + "not enough signers": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: notaryContractHash}}, + Scripts: []transaction.Witness{{}}, + }, + }, + "signers count and witnesses count mismatch": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: notaryContractHash}, {}}, + Scripts: []transaction.Witness{{}, {}, {}}, + }, + }, + "missing Notary witness": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: acc2.GetScriptHash()}}, + Scripts: []transaction.Witness{{}, {}}, + }, + }, + "unknown witness type": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + {}, + {}, + }, + }, + }, + "bad verification script": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: []byte{}, + VerificationScript: []byte{1, 2, 3}, + }, + {}, + }, + }, + }, + "several multisig witnesses": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: multisigScriptHash2}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + { + InvocationScript: sig, + VerificationScript: multisigScript2, + }, + {}, + }, + }, + nKeys: 2, + }, + "multisig + sig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + "sig + multisig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + "empty multisig + sig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: []byte{}, + VerificationScript: multisigScript1, + }, + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + "sig + empty multisig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + { + InvocationScript: []byte{}, + VerificationScript: multisigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + "multisig + empty sig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + { + InvocationScript: []byte{}, + VerificationScript: sigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + "empty sig + multisig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: []byte{}, + VerificationScript: sigScript1, + }, + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + "sig: bad nKeys": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: acc2.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + { + InvocationScript: sig, + VerificationScript: sigScript2, + }, + {}, + }, + }, + nKeys: 3, + }, + "multisig: bad witnesses count": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + }, + }, + nKeys: 2, + }, + "multisig: bad nKeys": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + {}, + }, + }, + nKeys: 2, + }, + } + + for name, errCase := range errCases { + t.Run(name, func(t *testing.T) { + checkErr(t, errCase.tx, errCase.nKeys) + }) + } + + testCases := map[string]struct { + tx *transaction.Transaction + nKeys uint8 + expectedType RequestType + expectedNSigs uint8 + expectedPubs keys.PublicKeys + }{ + "single sig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + {}, + }, + }, + nKeys: 1, + expectedType: Signature, + expectedNSigs: 1, + }, + "multiple sig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.GetScriptHash()}, {Account: acc2.GetScriptHash()}, {Account: acc3.GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: sigScript1, + }, + { + InvocationScript: []byte{}, + VerificationScript: []byte{}, + }, + { + InvocationScript: sig, + VerificationScript: sigScript3, + }, + {}, + }, + }, + nKeys: 3, + expectedType: Signature, + expectedNSigs: 3, + }, + "multisig 1 out of 3": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + {}, + }, + }, + nKeys: 3, + expectedType: MultiSignature, + expectedNSigs: 1, + expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}, + }, + "multisig 2 out of 3": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash2}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript2, + }, + {}, + }, + }, + nKeys: 3, + expectedType: MultiSignature, + expectedNSigs: 2, + expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}, + }, + "empty + multisig": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: acc1.PublicKey().GetScriptHash()}, {Account: multisigScriptHash1}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: []byte{}, + VerificationScript: []byte{}, + }, + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + {}, + }, + }, + nKeys: 3, + expectedType: MultiSignature, + expectedNSigs: 1, + expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}, + }, + "multisig + empty": { + tx: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: multisigScriptHash1}, {Account: acc1.PublicKey().GetScriptHash()}, {Account: notaryContractHash}}, + Scripts: []transaction.Witness{ + { + InvocationScript: sig, + VerificationScript: multisigScript1, + }, + { + InvocationScript: []byte{}, + VerificationScript: []byte{}, + }, + {}, + }, + }, + nKeys: 3, + expectedType: MultiSignature, + expectedNSigs: 1, + expectedPubs: keys.PublicKeys{acc1.PublicKey(), acc2.PublicKey(), acc3.PublicKey()}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + typ, nSigs, pubs, err := ntr.verifyIncompleteWitnesses(testCase.tx, testCase.nKeys) + require.NoError(t, err) + assert.Equal(t, testCase.expectedType, typ) + assert.Equal(t, testCase.expectedNSigs, nSigs) + assert.ElementsMatch(t, testCase.expectedPubs, pubs) + }) + } +} diff --git a/pkg/services/notary/request_type.go b/pkg/services/notary/request_type.go new file mode 100644 index 000000000..1fc4c8cda --- /dev/null +++ b/pkg/services/notary/request_type.go @@ -0,0 +1,13 @@ +package notary + +// RequestType represents the type of Notary request. +type RequestType byte + +const ( + // Unknown represents unknown request type which means that main tx witnesses are invalid. + Unknown RequestType = 0x00 + // Signature represents standard single signature request type. + Signature RequestType = 0x01 + // MultiSignature represents m out of n multisignature request type. + MultiSignature RequestType = 0x02 +) diff --git a/pkg/services/notary/testdata/notary1.json b/pkg/services/notary/testdata/notary1.json new file mode 100644 index 000000000..0a8d45251 --- /dev/null +++ b/pkg/services/notary/testdata/notary1.json @@ -0,0 +1 @@ +{"version":"3.0","accounts":[{"address":"NSbjd7dSePTZ6QpADAuM5722QpBmL5124W","key":"6PYVWTfkNCYvyQhyFLHH5dyRyT6jSi8u8Z8kn122PACfsDWi4QgkGm8FyW","label":"NotaryNode1","contract":{"script":"DCEDm5PmbOfVPmYXTSVW903XnOhhNBTsF9oDlVYusIH/ui0LQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false},{"address":"NisSvmSd2Lp28tjr8EqCZB5ahHDvBExo2j","key":"6PYLvgnZNwhiiZPiSCw3B3bHSFwbSXgh3MkGt4gL69MD8Sw7LMnuUgM9KQ","label":"three","contract":{"script":"DCEDHRWEIGXHCwUU2Fc7B0qrYPezXR0sfdEduRExyzIKVC8LQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false},{"address":"NRCCdGifyUWKnFotZhcgKpmxhhVJSJb94r","key":"6PYKXkuJ7G6bTj62bjy8fsBLF5okYNdAEBhKPCv8nmcALCtk2yPtBo835p","label":"four","contract":{"script":"DCECmUfs/gqKHd3AdJm5+Ev6zkubV8pP8DZzgu8+t5WdphILQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}} diff --git a/pkg/services/notary/testdata/notary2.json b/pkg/services/notary/testdata/notary2.json new file mode 100644 index 000000000..329c11390 --- /dev/null +++ b/pkg/services/notary/testdata/notary2.json @@ -0,0 +1 @@ +{"version":"3.0","accounts":[{"address":"NfFcJvWcHe8SSS92hNZhyQUJ6cg3pb36Tf","key":"6PYU2QoD52Xt9Z6QmNGUJWn89qUD1W6QqAL4Y8nfTWtTKvmVpQh8wsH6qY","label":"NotaryNode2","contract":{"script":"DCECIcKj0GFdv4b1NZrw9X6zLNLWzmNKAxtw6olIMZxpPRQLQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}}