From 2ab0e6c3994d3c6459e70393e86b7c782e3a1f16 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 10 Dec 2020 10:42:12 +0300 Subject: [PATCH 1/4] core: check stack length before returning `false` verification result We must be sure that stack has no other items before returning `false` verification result. It is an error in both cases, but by preserving the order we know exactly that it was correct `false` on stack. --- pkg/core/blockchain.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index aceabd0a7..2d2c00fbb 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1670,12 +1670,12 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa if err != nil { return 0, fmt.Errorf("%w: invalid return value", ErrVerificationFailed) } - if !res { - return 0, fmt.Errorf("%w: invalid signature", ErrVerificationFailed) - } if vm.Estack().Len() != 0 { return 0, fmt.Errorf("%w: expected exactly one returned value", ErrVerificationFailed) } + if !res { + return 0, fmt.Errorf("%w: invalid signature", ErrVerificationFailed) + } } else { return 0, fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed) } From 6d357c37936b59a52702483e1521b6e228ce3b8b Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 10 Dec 2020 10:56:02 +0300 Subject: [PATCH 2/4] core: return a special error from verifyHashAgainstScript It will help us to distinguish proper `false` verification result from various verification errors. --- pkg/core/blockchain.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 2d2c00fbb..a9ae9e1d4 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1595,6 +1595,7 @@ var ( ErrWitnessHashMismatch = errors.New("witness hash mismatch") ErrNativeContractWitness = errors.New("native contract witness must have empty verification script") ErrVerificationFailed = errors.New("signature check failed") + ErrInvalidSignature = fmt.Errorf("%w: invalid signature", ErrVerificationFailed) ErrUnknownVerificationContract = errors.New("unknown verification contract") ErrInvalidVerificationContract = errors.New("verification contract is missing `verify` method") ) @@ -1674,7 +1675,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa return 0, fmt.Errorf("%w: expected exactly one returned value", ErrVerificationFailed) } if !res { - return 0, fmt.Errorf("%w: invalid signature", ErrVerificationFailed) + return vm.GasConsumed(), ErrInvalidSignature } } else { return 0, fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed) From 501c0c93c6921cdd5c982bb1b309bbe7228fedd9 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 10 Dec 2020 11:07:29 +0300 Subject: [PATCH 3/4] core: take into account NotaryAssisted attributes during verification It's a bug, we have to reserve proper amount of GAS from verification gas limit for NotaryAssisted attributes. --- pkg/core/blockchain.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index a9ae9e1d4..fadded96b 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1695,6 +1695,13 @@ func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block } interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t) gasLimit := t.NetworkFee - int64(t.Size())*bc.FeePerByte() + if bc.P2PSigExtensionsEnabled() { + attrs := t.GetAttributes(transaction.NotaryAssistedT) + if len(attrs) != 0 { + na := attrs[0].Value.(*transaction.NotaryAssisted) + gasLimit -= (int64(na.NKeys) + 1) * transaction.NotaryServiceFeePerKey + } + } for i := range t.Signers { gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit) if err != nil { From 0b5cf784681c1bf6982f447680ff6b87b24afb55 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Fri, 27 Nov 2020 13:55:48 +0300 Subject: [PATCH 4/4] network: add notary request payload --- pkg/config/protocol_config.go | 5 +- pkg/consensus/consensus.go | 6 +- pkg/consensus/consensus_test.go | 4 +- pkg/core/blockchain.go | 154 +++++++++++++---- pkg/core/blockchain_test.go | 115 +++++++++++-- pkg/core/blockchainer/blockchainer.go | 9 +- pkg/core/blockchainer/policer.go | 8 + pkg/core/helper_test.go | 34 +++- pkg/core/mempool/mem_pool.go | 78 ++++++--- pkg/core/mempool/mem_pool_test.go | 179 ++++++++++++++++---- pkg/core/native/notary.go | 115 +++++++++++-- pkg/core/native/policy.go | 78 ++------- pkg/core/native/util.go | 44 +++++ pkg/core/native_notary_test.go | 47 ++++++ pkg/core/native_policy_test.go | 36 ++++ pkg/network/helper_test.go | 84 ++++++++-- pkg/network/message.go | 27 +-- pkg/network/notary_feer.go | 40 +++++ pkg/network/payload/inventory.go | 13 +- pkg/network/payload/inventory_test.go | 15 +- pkg/network/payload/notary_request.go | 149 +++++++++++++++++ pkg/network/payload/notary_request_test.go | 184 +++++++++++++++++++++ pkg/network/server.go | 102 ++++++++++-- pkg/network/server_test.go | 102 +++++++++++- 24 files changed, 1396 insertions(+), 232 deletions(-) create mode 100644 pkg/core/blockchainer/policer.go create mode 100644 pkg/network/notary_feer.go create mode 100644 pkg/network/payload/notary_request.go create mode 100644 pkg/network/payload/notary_request_test.go diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 1ac2f1c80..b1860b8c4 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -9,6 +9,9 @@ type ( ProtocolConfiguration struct { Magic netmode.Magic `yaml:"Magic"` MemPoolSize int `yaml:"MemPoolSize"` + // P2PNotaryRequestPayloadPoolSize specifies the memory pool size for P2PNotaryRequestPayloads. + // It is valid only if P2PSigExtensions are enabled. + P2PNotaryRequestPayloadPoolSize int `yaml:"P2PNotaryRequestPayloadPoolSize"` // KeepOnlyLatestState specifies if MPT should only store latest state. // If true, DB size will be smaller, but older roots won't be accessible. // This value should remain the same for the same database. @@ -17,7 +20,7 @@ type ( RemoveUntraceableBlocks bool `yaml:"RemoveUntraceableBlocks"` // MaxTraceableBlocks is the length of the chain accessible to smart contracts. MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"` - // P2PSigExtensions enables additional signature-related transaction attributes + // P2PSigExtensions enables additional signature-related logic. P2PSigExtensions bool `yaml:"P2PSigExtensions"` // ReservedAttributes allows to have reserved attributes range for experimental or private purposes. ReservedAttributes bool `yaml:"ReservedAttributes"` diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index 696b99e02..03a7a8efb 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -418,7 +418,7 @@ func (s *service) verifyBlock(b block.Block) bool { s.log.Warn("proposed block has already outdated") return false } - maxBlockSize := int(s.Chain.GetMaxBlockSize()) + maxBlockSize := int(s.Chain.GetPolicer().GetMaxBlockSize()) size := io.GetVarSize(coreb) if size > maxBlockSize { s.log.Warn("proposed block size exceeds policy max block size", @@ -428,7 +428,7 @@ func (s *service) verifyBlock(b block.Block) bool { } var fee int64 - var pool = mempool.New(len(coreb.Transactions)) + var pool = mempool.New(len(coreb.Transactions), 0) var mainPool = s.Chain.GetMemPool() for _, tx := range coreb.Transactions { var err error @@ -454,7 +454,7 @@ func (s *service) verifyBlock(b block.Block) bool { } } - maxBlockSysFee := s.Chain.GetMaxBlockSystemFee() + maxBlockSysFee := s.Chain.GetPolicer().GetMaxBlockSystemFee() if fee > maxBlockSysFee { s.log.Warn("proposed block system fee exceeds policy max block system fee", zap.Int("max system fee allowed", int(maxBlockSysFee)), diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go index 900138468..74f25b9f3 100644 --- a/pkg/consensus/consensus_test.go +++ b/pkg/consensus/consensus_test.go @@ -386,7 +386,7 @@ func TestVerifyBlock(t *testing.T) { require.False(t, srv.verifyBlock(&neoBlock{Block: *b})) }) t.Run("bad big size", func(t *testing.T) { - script := make([]byte, int(srv.Chain.GetMaxBlockSize())) + script := make([]byte, int(srv.Chain.GetPolicer().GetMaxBlockSize())) script[0] = byte(opcode.RET) tx := transaction.New(netmode.UnitTestNet, script, 100000) tx.ValidUntilBlock = 1 @@ -407,7 +407,7 @@ func TestVerifyBlock(t *testing.T) { t.Run("bad big sys fee", func(t *testing.T) { txes := make([]*transaction.Transaction, 2) for i := range txes { - txes[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.RET)}, srv.Chain.GetMaxBlockSystemFee()/2+1) + txes[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.RET)}, srv.Chain.GetPolicer().GetMaxBlockSystemFee()/2+1) txes[i].ValidUntilBlock = 1 addSender(t, txes[i]) signTx(t, srv.Chain.FeePerByte(), txes[i]) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index fadded96b..064aa0c48 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -13,6 +13,7 @@ import ( "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/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/mempool" @@ -42,9 +43,10 @@ const ( headerBatchCount = 2000 version = "0.1.0" - defaultMemPoolSize = 50000 - defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks - verificationGasLimit = 100000000 // 1 GAS + defaultMemPoolSize = 50000 + defaultP2PNotaryRequestPayloadPoolSize = 1000 + defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks + verificationGasLimit = 100000000 // 1 GAS ) var ( @@ -116,6 +118,10 @@ type Blockchain struct { memPool *mempool.Pool + // postBlock is a set of callback methods which should be run under the Blockchain lock after new block is persisted. + // Block's transactions are passed via mempool. + postBlock []func(blockchainer.Blockchainer, *mempool.Pool, *block.Block) + sbCommittee keys.PublicKeys log *zap.Logger @@ -151,6 +157,10 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L cfg.MemPoolSize = defaultMemPoolSize log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize)) } + if cfg.P2PSigExtensions && cfg.P2PNotaryRequestPayloadPoolSize <= 0 { + cfg.P2PNotaryRequestPayloadPoolSize = defaultP2PNotaryRequestPayloadPoolSize + log.Info("P2PNotaryRequestPayloadPool size is not set or wrong, setting default value", zap.Int("P2PNotaryRequestPayloadPoolSize", cfg.P2PNotaryRequestPayloadPoolSize)) + } if cfg.MaxTraceableBlocks == 0 { cfg.MaxTraceableBlocks = defaultMaxTraceableBlocks log.Info("MaxTraceableBlocks is not set or wrong, using default value", zap.Uint32("MaxTraceableBlocks", cfg.MaxTraceableBlocks)) @@ -164,7 +174,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), + memPool: mempool.New(cfg.MemPoolSize, 0), sbCommittee: committee, log: log, events: make(chan bcEvent), @@ -452,7 +462,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)) + mp = mempool.New(len(block.Transactions), 0) for _, tx := range block.Transactions { var err error // Transactions are verified before adding them @@ -464,7 +474,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error { continue } } else { - err = bc.verifyAndPoolTx(tx, mp) + err = bc.verifyAndPoolTx(tx, mp, bc) } if err != nil && bc.config.VerifyTransactions { return fmt.Errorf("transaction %s failed to verify: %w", tx.Hash().StringLE(), err) @@ -720,6 +730,13 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error bc.lock.Unlock() return fmt.Errorf("failed to call OnPersistEnd for Policy native contract: %w", err) } + if bc.P2PSigExtensionsEnabled() { + err := bc.contracts.Notary.OnPersistEnd(bc.dao) + if err != nil { + bc.lock.Unlock() + return fmt.Errorf("failed to call OnPersistEnd for Notary native contract: %w", err) + } + } if err := bc.contracts.Designate.OnPersistEnd(bc.dao); err != nil { bc.lock.Unlock() return err @@ -733,7 +750,10 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error } bc.topBlock.Store(block) atomic.StoreUint32(&bc.blockHeight, block.Index) - bc.memPool.RemoveStale(func(tx *transaction.Transaction) bool { return bc.isTxStillRelevant(tx, txpool) }, bc) + bc.memPool.RemoveStale(func(tx *transaction.Transaction) bool { return bc.IsTxStillRelevant(tx, txpool, false) }, bc) + for _, f := range bc.postBlock { + f(bc, txpool, block) + } bc.lock.Unlock() updateBlockHeightMetric(block.Index) @@ -934,6 +954,24 @@ func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint return &neo.Balance, neo.LastUpdatedBlock } +// GetNotaryBalance returns Notary deposit amount for the specified account. +func (bc *Blockchain) GetNotaryBalance(acc util.Uint160) *big.Int { + return bc.contracts.Notary.BalanceOf(bc.dao, acc) +} + +// GetNotaryContractScriptHash returns Notary native contract hash. +func (bc *Blockchain) GetNotaryContractScriptHash() util.Uint160 { + if bc.P2PSigExtensionsEnabled() { + return bc.contracts.Notary.Hash + } + return util.Uint160{} +} + +// GetNotaryDepositExpiration returns Notary deposit expiration height for the specified account. +func (bc *Blockchain) GetNotaryDepositExpiration(acc util.Uint160) uint32 { + return bc.contracts.Notary.ExpirationOf(bc.dao, acc) +} + // LastBatch returns last persisted storage batch. func (bc *Blockchain) LastBatch() *storage.MemBatch { return bc.lastBatch @@ -1204,16 +1242,6 @@ func (bc *Blockchain) FeePerByte() int64 { return bc.contracts.Policy.GetFeePerByteInternal(bc.dao) } -// GetMaxBlockSize returns maximum allowed block size from native Policy contract. -func (bc *Blockchain) GetMaxBlockSize() uint32 { - return bc.contracts.Policy.GetMaxBlockSizeInternal(bc.dao) -} - -// GetMaxBlockSystemFee returns maximum block system fee from native Policy contract. -func (bc *Blockchain) GetMaxBlockSystemFee() int64 { - return bc.contracts.Policy.GetMaxBlockSystemFeeInternal(bc.dao) -} - // GetMemPool returns the memory pool of the blockchain. func (bc *Blockchain) GetMemPool() *mempool.Pool { return bc.memPool @@ -1279,9 +1307,10 @@ var ( // verifyAndPoolTx verifies whether a transaction is bonafide or not and tries // to add it to the mempool given. -func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.Pool) error { +func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.Pool, feer mempool.Feer, data ...interface{}) error { height := bc.BlockHeight() - if t.ValidUntilBlock <= height || t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement { + isPartialTx := data != nil + if t.ValidUntilBlock <= height || !isPartialTx && t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement { return fmt.Errorf("%w: ValidUntilBlock = %d, current height = %d", ErrTxExpired, t.ValidUntilBlock, height) } // Policying. @@ -1316,14 +1345,14 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return err } } - err := bc.verifyTxWitnesses(t, nil) + err := bc.verifyTxWitnesses(t, nil, isPartialTx) if err != nil { return err } - if err := bc.verifyTxAttributes(t); err != nil { + if err := bc.verifyTxAttributes(t, isPartialTx); err != nil { return err } - err = pool.Add(t, bc) + err = pool.Add(t, feer, data...) if err != nil { switch { case errors.Is(err, mempool.ErrConflict): @@ -1344,7 +1373,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return nil } -func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { +func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction, isPartialTx bool) error { for i := range tx.Attributes { switch attrType := tx.Attributes[i].Type; attrType { case transaction.HighPriority: @@ -1384,9 +1413,19 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if !bc.config.P2PSigExtensions { return fmt.Errorf("%w: NotValidBefore attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) } - nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore) - if height := bc.BlockHeight(); height < nvb.Height { - return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb.Height, height) + nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore).Height + if isPartialTx { + maxNVBDelta := bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao) + if bc.BlockHeight()+maxNVBDelta < nvb { + return fmt.Errorf("%w: partially-filled transaction should become valid not less then %d blocks after current chain's height %d", ErrInvalidAttribute, maxNVBDelta, bc.BlockHeight()) + } + if nvb+maxNVBDelta < tx.ValidUntilBlock { + return fmt.Errorf("%w: partially-filled transaction should be valid during less than %d blocks", ErrInvalidAttribute, maxNVBDelta) + } + } else { + if height := bc.BlockHeight(); height < nvb { + return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb, height) + } } case transaction.ConflictsT: if !bc.config.P2PSigExtensions { @@ -1412,13 +1451,13 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { return nil } -// isTxStillRelevant is a callback for mempool transaction filtering after the +// IsTxStillRelevant is a callback for mempool transaction filtering after the // new block addition. It returns false for transactions added by the new block // (passed via txpool) and does witness reverification for non-standard // contracts. It operates under the assumption that full transaction verification // was already done so we don't need to check basic things like size, input/output // correctness, presence in blocks before the new one, etc. -func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool) bool { +func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool { var recheckWitness bool var curheight = bc.BlockHeight() @@ -1432,7 +1471,7 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *memp } else if txpool.HasConflicts(t, bc) { return false } - if err := bc.verifyTxAttributes(t); err != nil { + if err := bc.verifyTxAttributes(t, isPartialTx); err != nil { return false } for i := range t.Scripts { @@ -1442,7 +1481,7 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *memp } } if recheckWitness { - return bc.verifyTxWitnesses(t, nil) == nil + return bc.verifyTxWitnesses(t, nil, isPartialTx) == nil } return true @@ -1525,10 +1564,10 @@ 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) + var mp = mempool.New(1, 0) bc.lock.RLock() defer bc.lock.RUnlock() - return bc.verifyAndPoolTx(t, mp) + return bc.verifyAndPoolTx(t, mp, bc) } // PoolTx verifies and tries to add given transaction into the mempool. If not @@ -1545,7 +1584,21 @@ func (bc *Blockchain) PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) if len(pools) == 1 { pool = pools[0] } - return bc.verifyAndPoolTx(t, pool) + return bc.verifyAndPoolTx(t, pool, bc) +} + +// PoolTxWithData verifies and tries to add given transaction with additional data into the mempool. +func (bc *Blockchain) PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc blockchainer.Blockchainer, tx *transaction.Transaction, data interface{}) error) error { + bc.lock.RLock() + defer bc.lock.RUnlock() + + if verificationFunction != nil { + err := verificationFunction(bc, t, data) + if err != nil { + return err + } + } + return bc.verifyAndPoolTx(t, mp, feer, data) } //GetStandByValidators returns validators from the configuration. @@ -1689,7 +1742,7 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa // is used for easy interop access and can be omitted for transactions that are // not yet added into any block. // Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). -func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block) error { +func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block, isPartialTx bool) error { if len(t.Signers) != len(t.Scripts) { return fmt.Errorf("%w: %d vs %d", ErrTxInvalidWitnessNum, len(t.Signers), len(t.Scripts)) } @@ -1704,7 +1757,8 @@ func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block } for i := range t.Signers { gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit) - if err != nil { + if err != nil && + !(i == 0 && isPartialTx && errors.Is(err, ErrInvalidSignature)) { // it's OK for partially-filled transaction with dummy first witness. return fmt.Errorf("witness #%d: %w", i, err) } gasLimit -= gasConsumed @@ -1757,3 +1811,33 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block * func (bc *Blockchain) P2PSigExtensionsEnabled() bool { return bc.config.P2PSigExtensions } + +// RegisterPostBlock appends provided function to the list of functions which should be run after new block +// is stored. +func (bc *Blockchain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) { + bc.postBlock = append(bc.postBlock, f) +} + +// -- start Policer. + +// GetPolicer provides access to policy values via Policer interface. +func (bc *Blockchain) GetPolicer() blockchainer.Policer { + return bc +} + +// GetMaxBlockSize returns maximum allowed block size from native Policy contract. +func (bc *Blockchain) GetMaxBlockSize() uint32 { + return bc.contracts.Policy.GetMaxBlockSizeInternal(bc.dao) +} + +// GetMaxBlockSystemFee returns maximum block system fee from native Policy contract. +func (bc *Blockchain) GetMaxBlockSystemFee() int64 { + return bc.contracts.Policy.GetMaxBlockSystemFeeInternal(bc.dao) +} + +// GetMaxVerificationGAS returns maximum verification GAS Policy limit. +func (bc *Blockchain) GetMaxVerificationGAS() int64 { + return bc.contracts.Policy.GetMaxVerificationGas(bc.dao) +} + +// -- end Policer. diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index b243f9385..d63e0a803 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -14,6 +14,7 @@ import ( "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/chaindump" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" @@ -447,7 +448,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) + bc.memPool = mempool.New(1, 0) tx1 := bc.newTestTx(h, testScript) tx1.NetworkFee += 10000 // Give it more priority. require.NoError(t, accs[0].SignTx(tx1)) @@ -926,6 +927,96 @@ func TestVerifyTx(t *testing.T) { }) }) }) + t.Run("Partially-filled transaction", func(t *testing.T) { + bc.config.P2PSigExtensions = true + getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction { + tx := bc.newTestTx(h, testScript) + tx.ValidUntilBlock = validUntil + tx.Attributes = []transaction.Attribute{ + { + Type: transaction.NotValidBeforeT, + Value: &transaction.NotValidBefore{Height: nvb}, + }, + { + Type: transaction.NotaryAssistedT, + Value: &transaction.NotaryAssisted{NKeys: 0}, + }, + } + tx.Signers = []transaction.Signer{ + { + Account: bc.contracts.Notary.Hash, + Scopes: transaction.None, + }, + { + Account: testchain.MultisigScriptHash(), + Scopes: transaction.None, + }, + } + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(testchain.MultisigVerificationScript()) + tx.NetworkFee = netFee + // multisig witness verification price + int64(size)*bc.FeePerByte() + // fee for unsigned size + int64(sizeDelta)*bc.FeePerByte() + //fee for multisig size + 66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script) + 2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation) + transaction.NotaryServiceFeePerKey + // fee for Notary attribute + fee.Opcode( // Notary verification script + opcode.PUSHDATA1, opcode.RET, // invocation script + opcode.DEPTH, opcode.PACK, opcode.PUSHDATA1, opcode.RET, // arguments for native verification call + opcode.PUSHDATA1, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call + native.NotaryVerificationPrice // Notary witness verification price + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), + VerificationScript: []byte{}, + }, + { + InvocationScript: testchain.Sign(tx.GetSignedPart()), + VerificationScript: testchain.MultisigVerificationScript(), + }, + } + return tx + } + + mp := mempool.New(10, 1) + verificationF := func(bc blockchainer.Blockchainer, tx *transaction.Transaction, data interface{}) error { + if data.(int) > 5 { + return errors.New("bad data") + } + return nil + } + t.Run("failed pre-verification", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) + require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity. + }) + t.Run("GasLimitExceeded during witness verification", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) + tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx + tx.Scripts = []transaction.Witness{ + { + InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), + VerificationScript: []byte{}, + }, + { + InvocationScript: testchain.Sign(tx.GetSignedPart()), + VerificationScript: testchain.MultisigVerificationScript(), + }, + } + require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) + }) + t.Run("bad NVB: too big", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1, bc.blockHeight+1) + require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute)) + }) + t.Run("bad ValidUntilBlock: too small", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1) + require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute)) + }) + t.Run("good", func(t *testing.T) { + tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1) + require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF)) + }) + }) } func TestVerifyHashAgainstScript(t *testing.T) { @@ -1021,9 +1112,9 @@ func TestIsTxStillRelevant(t *testing.T) { tx := newTx(t) require.NoError(t, testchain.SignTx(bc, tx)) - require.True(t, bc.isTxStillRelevant(tx, nil)) + require.True(t, bc.IsTxStillRelevant(tx, nil, false)) require.NoError(t, bc.AddBlock(bc.newBlock())) - require.False(t, bc.isTxStillRelevant(tx, nil)) + require.False(t, bc.IsTxStillRelevant(tx, nil, false)) }) t.Run("tx is already persisted", func(t *testing.T) { @@ -1031,9 +1122,9 @@ func TestIsTxStillRelevant(t *testing.T) { tx.ValidUntilBlock = bc.BlockHeight() + 2 require.NoError(t, testchain.SignTx(bc, tx)) - require.True(t, bc.isTxStillRelevant(tx, nil)) + require.True(t, bc.IsTxStillRelevant(tx, nil, false)) require.NoError(t, bc.AddBlock(bc.newBlock(tx))) - require.False(t, bc.isTxStillRelevant(tx, nil)) + require.False(t, bc.IsTxStillRelevant(tx, nil, false)) }) t.Run("tx with Conflicts attribute", func(t *testing.T) { @@ -1047,9 +1138,9 @@ func TestIsTxStillRelevant(t *testing.T) { }} require.NoError(t, testchain.SignTx(bc, tx2)) - require.True(t, bc.isTxStillRelevant(tx1, mp)) - require.NoError(t, bc.verifyAndPoolTx(tx2, mp)) - require.False(t, bc.isTxStillRelevant(tx1, mp)) + require.True(t, bc.IsTxStillRelevant(tx1, mp, false)) + require.NoError(t, bc.verifyAndPoolTx(tx2, mp, bc)) + require.False(t, bc.IsTxStillRelevant(tx1, mp, false)) }) t.Run("NotValidBefore", func(t *testing.T) { tx3 := newTx(t) @@ -1060,9 +1151,9 @@ func TestIsTxStillRelevant(t *testing.T) { tx3.ValidUntilBlock = bc.BlockHeight() + 2 require.NoError(t, testchain.SignTx(bc, tx3)) - require.False(t, bc.isTxStillRelevant(tx3, nil)) + require.False(t, bc.IsTxStillRelevant(tx3, nil, false)) require.NoError(t, bc.AddBlock(bc.newBlock())) - require.True(t, bc.isTxStillRelevant(tx3, nil)) + require.True(t, bc.IsTxStillRelevant(tx3, nil, false)) }) t.Run("contract witness check fails", func(t *testing.T) { src := fmt.Sprintf(`package verify @@ -1087,9 +1178,9 @@ func TestIsTxStillRelevant(t *testing.T) { require.NoError(t, testchain.SignTx(bc, tx)) tx.Scripts = append(tx.Scripts, transaction.Witness{}) - require.True(t, bc.isTxStillRelevant(tx, mp)) + require.True(t, bc.IsTxStillRelevant(tx, mp, false)) require.NoError(t, bc.AddBlock(bc.newBlock())) - require.False(t, bc.isTxStillRelevant(tx, mp)) + require.False(t, bc.IsTxStillRelevant(tx, mp, false)) }) } diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 422e1ff46..bcbc2c780 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -25,6 +25,7 @@ type Blockchainer interface { AddStateRoot(r *state.MPTRoot) error CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error) Close() + IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool HeaderHeight() uint32 GetBlock(hash util.Uint256) (*block.Block, error) GetCommittee() (keys.PublicKeys, error) @@ -40,9 +41,13 @@ type Blockchainer interface { HasBlock(util.Uint256) bool HasTransaction(util.Uint256) bool GetAppExecResults(util.Uint256, trigger.Type) ([]state.AppExecResult, error) + GetNotaryDepositExpiration(acc util.Uint160) uint32 GetNativeContractScriptHash(string) (util.Uint160, error) GetNextBlockValidators() ([]*keys.PublicKey, error) GetNEP17Balances(util.Uint160) *state.NEP17Balances + GetNotaryContractScriptHash() util.Uint160 + GetNotaryBalance(acc util.Uint160) *big.Int + GetPolicer() Policer GetValidators() ([]*keys.PublicKey, error) GetStandByCommittee() keys.PublicKeys GetStandByValidators() keys.PublicKeys @@ -53,9 +58,9 @@ type Blockchainer interface { GetTestVM(tx *transaction.Transaction, b *block.Block) *vm.VM GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) mempool.Feer // fee interface - GetMaxBlockSize() uint32 - GetMaxBlockSystemFee() int64 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)) SubscribeForBlocks(ch chan<- *block.Block) SubscribeForExecutions(ch chan<- *state.AppExecResult) SubscribeForNotifications(ch chan<- *state.NotificationEvent) diff --git a/pkg/core/blockchainer/policer.go b/pkg/core/blockchainer/policer.go new file mode 100644 index 000000000..56888de1d --- /dev/null +++ b/pkg/core/blockchainer/policer.go @@ -0,0 +1,8 @@ +package blockchainer + +// Policer is an interface that abstracts the implementation of policy methods. +type Policer interface { + GetMaxBlockSize() uint32 + GetMaxBlockSystemFee() int64 + GetMaxVerificationGAS() int64 +} diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index e1b6a6de7..653c9d2a4 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -439,6 +439,38 @@ func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, me return &res[0], nil } +func invokeContractMethodBy(t *testing.T, chain *Blockchain, signer *wallet.Account, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) { + var ( + netfee int64 = 1000_0000 + sysfee int64 = 1_0000_0000 + ) + transferTx := transferTokenFromMultisigAccount(t, chain, signer.PrivateKey().PublicKey().GetScriptHash(), chain.contracts.GAS.Hash, sysfee+netfee+1000_0000, nil) + 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)) + + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, hash, method, args...) + if w.Err != nil { + return nil, w.Err + } + script := w.Bytes() + tx := transaction.New(chain.GetConfig().Magic, script, sysfee) + tx.ValidUntilBlock = chain.blockHeight + 1 + tx.Signers = []transaction.Signer{ + {Account: signer.PrivateKey().PublicKey().GetScriptHash()}, + } + tx.NetworkFee = netfee + err = signer.SignTx(tx) + require.NoError(t, err) + require.NoError(t, chain.AddBlock(chain.newBlock(tx))) + + res, err = chain.GetAppExecResults(tx.Hash(), trigger.Application) + require.NoError(t, err) + return &res[0], nil +} + 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 @@ -451,7 +483,7 @@ func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, token } func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) { - require.Equal(t, vm.HaltState, result.VMState) + require.Equal(t, vm.HaltState, result.VMState, result.FaultException) require.Equal(t, 1, len(result.Stack)) require.Equal(t, expected, result.Stack[0]) } diff --git a/pkg/core/mempool/mem_pool.go b/pkg/core/mempool/mem_pool.go index 1689af8bb..15be93292 100644 --- a/pkg/core/mempool/mem_pool.go +++ b/pkg/core/mempool/mem_pool.go @@ -39,6 +39,7 @@ var ( type item struct { txn *transaction.Transaction blockStamp uint32 + data interface{} } // items is a slice of item. @@ -64,9 +65,10 @@ type Pool struct { capacity int feePerByte int64 + payerIndex int resendThreshold uint32 - resendFunc func(*transaction.Transaction) + resendFunc func(*transaction.Transaction, interface{}) } func (p items) Len() int { return len(p) } @@ -149,11 +151,12 @@ func (mp *Pool) HasConflicts(t *transaction.Transaction, fee Feer) bool { // tryAddSendersFee tries to add system fee and network fee to the total sender`s fee in mempool // and returns false if both balance check is required and sender has not enough GAS to pay func (mp *Pool) tryAddSendersFee(tx *transaction.Transaction, feer Feer, needCheck bool) bool { - senderFee, ok := mp.fees[tx.Sender()] + payer := tx.Signers[mp.payerIndex].Account + senderFee, ok := mp.fees[payer] if !ok { - senderFee.balance = feer.GetUtilityTokenBalance(tx.Sender()) + senderFee.balance = feer.GetUtilityTokenBalance(payer) senderFee.feeSum = big.NewInt(0) - mp.fees[tx.Sender()] = senderFee + mp.fees[payer] = senderFee } if needCheck { newFeeSum, err := checkBalance(tx, senderFee) @@ -182,11 +185,14 @@ func checkBalance(tx *transaction.Transaction, balance utilityBalanceAndFees) (* } // Add tries to add given transaction to the Pool. -func (mp *Pool) Add(t *transaction.Transaction, fee Feer) error { +func (mp *Pool) Add(t *transaction.Transaction, fee Feer, data ...interface{}) error { var pItem = item{ txn: t, blockStamp: fee.BlockHeight(), } + if data != nil { + pItem.data = data[0] + } mp.lock.Lock() if mp.containsKey(t.Hash()) { mp.lock.Unlock() @@ -281,14 +287,16 @@ func (mp *Pool) removeInternal(hash util.Uint256, feer Feer) { break } } + itm := mp.verifiedTxes[num] if num < len(mp.verifiedTxes)-1 { mp.verifiedTxes = append(mp.verifiedTxes[:num], mp.verifiedTxes[num+1:]...) } else if num == len(mp.verifiedTxes)-1 { mp.verifiedTxes = mp.verifiedTxes[:num] } - senderFee := mp.fees[tx.Sender()] + payer := itm.txn.Signers[mp.payerIndex].Account + senderFee := mp.fees[payer] senderFee.feeSum.Sub(senderFee.feeSum, big.NewInt(tx.SystemFee+tx.NetworkFee)) - mp.fees[tx.Sender()] = senderFee + mp.fees[payer] = senderFee if feer.P2PSigExtensionsEnabled() { // remove all conflicting hashes from mp.conflicts list mp.removeConflictsOf(tx) @@ -314,7 +322,7 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) mp.conflicts = make(map[util.Uint256][]util.Uint256) } height := feer.BlockHeight() - var staleTxs []*transaction.Transaction + 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) @@ -325,11 +333,11 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) } } if mp.resendThreshold != 0 { - // tx is resend at resendThreshold, 2*resendThreshold, 4*resendThreshold ... + // item is resend at resendThreshold, 2*resendThreshold, 4*resendThreshold ... // so quotient must be a power of two. diff := (height - itm.blockStamp) if diff%mp.resendThreshold == 0 && bits.OnesCount32(diff/mp.resendThreshold) == 1 { - staleTxs = append(staleTxs, itm.txn) + staleItems = append(staleItems, itm) } } } else { @@ -339,8 +347,8 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer) } } } - if len(staleTxs) != 0 { - go mp.resendStaleTxs(staleTxs) + if len(staleItems) != 0 { + go mp.resendStaleItems(staleItems) } mp.verifiedTxes = newVerifiedTxes mp.lock.Unlock() @@ -366,11 +374,12 @@ func (mp *Pool) checkPolicy(tx *transaction.Transaction, policyChanged bool) boo } // New returns a new Pool struct. -func New(capacity int) *Pool { +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), @@ -379,16 +388,16 @@ func New(capacity int) *Pool { // SetResendThreshold sets threshold after which transaction will be considered stale // and returned for retransmission by `GetStaleTransactions`. -func (mp *Pool) SetResendThreshold(h uint32, f func(*transaction.Transaction)) { +func (mp *Pool) SetResendThreshold(h uint32, f func(*transaction.Transaction, interface{})) { mp.lock.Lock() defer mp.lock.Unlock() mp.resendThreshold = h mp.resendFunc = f } -func (mp *Pool) resendStaleTxs(txs []*transaction.Transaction) { - for i := range txs { - mp.resendFunc(txs[i]) +func (mp *Pool) resendStaleItems(items []item) { + for i := range items { + mp.resendFunc(items[i].txn, items[i].data) } } @@ -403,6 +412,30 @@ func (mp *Pool) TryGetValue(hash util.Uint256) (*transaction.Transaction, bool) return nil, false } +// TryGetData returns data associated with the specified transaction if it exists in the memory pool. +func (mp *Pool) TryGetData(hash util.Uint256) (interface{}, bool) { + mp.lock.RLock() + defer mp.lock.RUnlock() + if tx, ok := mp.verifiedMap[hash]; ok { + itm := item{txn: tx} + n := sort.Search(len(mp.verifiedTxes), func(n int) bool { + return itm.CompareTo(mp.verifiedTxes[n]) >= 0 + }) + if n < len(mp.verifiedTxes) { + for i := n; i < len(mp.verifiedTxes); i++ { // items may have equal priority, so `n` is the left bound of the items which are as prioritized as the desired `itm`. + if mp.verifiedTxes[i].txn.Hash() == hash { + return mp.verifiedTxes[i].data, ok + } + if itm.CompareTo(mp.verifiedTxes[i]) != 0 { + break + } + } + } + } + + return nil, false +} + // GetVerifiedTransactions returns a slice of transactions with their fees. func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction { mp.lock.RLock() @@ -420,9 +453,10 @@ func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction { // checkTxConflicts is an internal unprotected version of Verify. It takes into // consideration conflicting transactions which are about to be removed from mempool. func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*transaction.Transaction, error) { - actualSenderFee, ok := mp.fees[tx.Sender()] + payer := tx.Signers[mp.payerIndex].Account + actualSenderFee, ok := mp.fees[payer] if !ok { - actualSenderFee.balance = fee.GetUtilityTokenBalance(tx.Sender()) + actualSenderFee.balance = fee.GetUtilityTokenBalance(payer) actualSenderFee.feeSum = big.NewInt(0) } @@ -434,7 +468,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran if conflictingHashes, ok := mp.conflicts[tx.Hash()]; ok { for _, hash := range conflictingHashes { existingTx := mp.verifiedMap[hash] - if existingTx.HasSigner(tx.Sender()) && existingTx.NetworkFee > tx.NetworkFee { + if existingTx.HasSigner(payer) && existingTx.NetworkFee > tx.NetworkFee { return nil, fmt.Errorf("%w: conflicting transaction %s has bigger network fee", ErrConflictsAttribute, existingTx.Hash().StringBE()) } conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx) @@ -447,7 +481,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran if !ok { continue } - if !tx.HasSigner(existingTx.Sender()) { + if !tx.HasSigner(existingTx.Signers[mp.payerIndex].Account) { return nil, fmt.Errorf("%w: not signed by the sender of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE()) } if existingTx.NetworkFee >= tx.NetworkFee { @@ -461,7 +495,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran feeSum: new(big.Int).Set(actualSenderFee.feeSum), } for _, conflictingTx := range conflictsToBeRemoved { - if conflictingTx.Sender().Equals(tx.Sender()) { + if conflictingTx.Signers[mp.payerIndex].Account.Equals(payer) { expectedSenderFee.feeSum.Sub(expectedSenderFee.feeSum, big.NewInt(conflictingTx.SystemFee+conflictingTx.NetworkFee)) } } diff --git a/pkg/core/mempool/mem_pool_test.go b/pkg/core/mempool/mem_pool_test.go index e8167dc01..6dc3d2a6b 100644 --- a/pkg/core/mempool/mem_pool_test.go +++ b/pkg/core/mempool/mem_pool_test.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/internal/random" "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/network/payload" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/stretchr/testify/assert" @@ -20,10 +21,9 @@ type FeerStub struct { feePerByte int64 p2pSigExt bool blockHeight uint32 + balance int64 } -var balance = big.NewInt(10000000) - func (fs *FeerStub) FeePerByte() int64 { return fs.feePerByte } @@ -33,7 +33,7 @@ func (fs *FeerStub) BlockHeight() uint32 { } func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int { - return balance + return big.NewInt(fs.balance) } func (fs *FeerStub) P2PSigExtensionsEnabled() bool { @@ -41,7 +41,7 @@ func (fs *FeerStub) P2PSigExtensionsEnabled() bool { } func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) { - mp := New(10) + mp := New(10, 0) tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx.Nonce = 0 tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}} @@ -62,7 +62,7 @@ func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) { } func TestMemPoolRemoveStale(t *testing.T) { - mp := New(5) + mp := New(5, 0) txs := make([]*transaction.Transaction, 5) for i := range txs { txs[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) @@ -72,7 +72,7 @@ func TestMemPoolRemoveStale(t *testing.T) { } staleTxs := make(chan *transaction.Transaction, 5) - f := func(tx *transaction.Transaction) { + f := func(tx *transaction.Transaction, _ interface{}) { staleTxs <- tx } mp.SetResendThreshold(5, f) @@ -111,9 +111,9 @@ func TestMemPoolAddRemove(t *testing.T) { } func TestOverCapacity(t *testing.T) { - var fs = &FeerStub{} + var fs = &FeerStub{balance: 10000000} const mempoolSize = 10 - mp := New(mempoolSize) + mp := New(mempoolSize, 0) for i := 0; i < mempoolSize; i++ { tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) @@ -186,7 +186,7 @@ func TestOverCapacity(t *testing.T) { func TestGetVerified(t *testing.T) { var fs = &FeerStub{} const mempoolSize = 10 - mp := New(mempoolSize) + mp := New(mempoolSize, 0) txes := make([]*transaction.Transaction, 0, mempoolSize) for i := 0; i < mempoolSize; i++ { @@ -210,7 +210,7 @@ func TestGetVerified(t *testing.T) { func TestRemoveStale(t *testing.T) { var fs = &FeerStub{} const mempoolSize = 10 - mp := New(mempoolSize) + mp := New(mempoolSize, 0) txes1 := make([]*transaction.Transaction, 0, mempoolSize/2) txes2 := make([]*transaction.Transaction, 0, mempoolSize/2) @@ -243,50 +243,51 @@ func TestRemoveStale(t *testing.T) { } func TestMemPoolFees(t *testing.T) { - mp := New(10) + mp := New(10, 0) + fs := &FeerStub{balance: 10000000} sender0 := util.Uint160{1, 2, 3} tx0 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) - tx0.NetworkFee = balance.Int64() + 1 + tx0.NetworkFee = fs.balance + 1 tx0.Signers = []transaction.Signer{{Account: sender0}} // insufficient funds to add transaction, and balance shouldn't be stored - require.Equal(t, false, mp.Verify(tx0, &FeerStub{})) - require.Error(t, mp.Add(tx0, &FeerStub{})) + require.Equal(t, false, mp.Verify(tx0, fs)) + require.Error(t, mp.Add(tx0, fs)) require.Equal(t, 0, len(mp.fees)) - balancePart := new(big.Int).Div(balance, big.NewInt(4)) + balancePart := new(big.Int).Div(big.NewInt(fs.balance), big.NewInt(4)) // no problems with adding another transaction with lower fee tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx1.NetworkFee = balancePart.Int64() tx1.Signers = []transaction.Signer{{Account: sender0}} - require.NoError(t, mp.Add(tx1, &FeerStub{})) + require.NoError(t, mp.Add(tx1, fs)) require.Equal(t, 1, len(mp.fees)) require.Equal(t, utilityBalanceAndFees{ - balance: balance, + balance: big.NewInt(fs.balance), feeSum: big.NewInt(tx1.NetworkFee), }, mp.fees[sender0]) // balance shouldn't change after adding one more transaction tx2 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) - tx2.NetworkFee = new(big.Int).Sub(balance, balancePart).Int64() + tx2.NetworkFee = new(big.Int).Sub(big.NewInt(fs.balance), balancePart).Int64() tx2.Signers = []transaction.Signer{{Account: sender0}} - require.NoError(t, mp.Add(tx2, &FeerStub{})) + require.NoError(t, mp.Add(tx2, fs)) require.Equal(t, 2, len(mp.verifiedTxes)) require.Equal(t, 1, len(mp.fees)) require.Equal(t, utilityBalanceAndFees{ - balance: balance, - feeSum: balance, + balance: big.NewInt(fs.balance), + feeSum: big.NewInt(fs.balance), }, mp.fees[sender0]) // can't add more transactions as we don't have enough GAS tx3 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx3.NetworkFee = 1 tx3.Signers = []transaction.Signer{{Account: sender0}} - require.Equal(t, false, mp.Verify(tx3, &FeerStub{})) - require.Error(t, mp.Add(tx3, &FeerStub{})) + require.Equal(t, false, mp.Verify(tx3, fs)) + require.Error(t, mp.Add(tx3, fs)) require.Equal(t, 1, len(mp.fees)) require.Equal(t, utilityBalanceAndFees{ - balance: balance, - feeSum: balance, + balance: big.NewInt(fs.balance), + feeSum: big.NewInt(fs.balance), }, mp.fees[sender0]) // check whether sender's fee updates correctly @@ -295,10 +296,10 @@ func TestMemPoolFees(t *testing.T) { return true } return false - }, &FeerStub{}) + }, fs) require.Equal(t, 1, len(mp.fees)) require.Equal(t, utilityBalanceAndFees{ - balance: balance, + balance: big.NewInt(fs.balance), feeSum: big.NewInt(tx2.NetworkFee), }, mp.fees[sender0]) @@ -308,12 +309,13 @@ func TestMemPoolFees(t *testing.T) { return true } return false - }, &FeerStub{}) + }, fs) require.Equal(t, 0, len(mp.fees)) } func TestMempoolItemsOrder(t *testing.T) { sender0 := util.Uint160{1, 2, 3} + balance := big.NewInt(10000000) tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx1.NetworkFee = new(big.Int).Div(balance, big.NewInt(8)).Int64() @@ -352,9 +354,9 @@ func TestMempoolItemsOrder(t *testing.T) { } func TestMempoolAddRemoveOracleResponse(t *testing.T) { - mp := New(5) + mp := New(5, 0) nonce := uint32(0) - fs := &FeerStub{} + fs := &FeerStub{balance: 10000} newTx := func(netFee int64, id uint64) *transaction.Transaction { tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx.NetworkFee = netFee @@ -406,9 +408,9 @@ func TestMempoolAddRemoveOracleResponse(t *testing.T) { func TestMempoolAddRemoveConflicts(t *testing.T) { capacity := 6 - mp := New(capacity) + mp := New(capacity, 0) var ( - fs = &FeerStub{p2pSigExt: true} + fs = &FeerStub{p2pSigExt: true, balance: 100000} nonce uint32 = 1 ) getConflictsTx := func(netFee int64, hashes ...util.Uint256) *transaction.Transaction { @@ -524,3 +526,116 @@ func TestMempoolAddRemoveConflicts(t *testing.T) { require.Equal(t, false, ok) require.True(t, errors.Is(mp.Add(tx13, fs), ErrConflictsAttribute)) } + +func TestMempoolAddWithDataGetData(t *testing.T) { + var ( + smallNetFee int64 = 3 + nonce uint32 + ) + fs := &FeerStub{ + feePerByte: 0, + p2pSigExt: true, + blockHeight: 5, + balance: 100, + } + mp := New(10, 1) + newTx := func(t *testing.T, netFee int64) *transaction.Transaction { + tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.RET)}, 0) + tx.Signers = []transaction.Signer{{}, {}} + tx.NetworkFee = netFee + nonce++ + tx.Nonce = nonce + return tx + } + + // bad, insufficient deposit + r1 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, fs.balance+1), + } + require.True(t, errors.Is(mp.Add(r1.FallbackTransaction, fs, r1), ErrInsufficientFunds)) + + // good + r2 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, smallNetFee), + } + require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2)) + require.True(t, mp.ContainsKey(r2.FallbackTransaction.Hash())) + data, ok := mp.TryGetData(r2.FallbackTransaction.Hash()) + require.True(t, ok) + require.Equal(t, r2, data) + + // bad, already in pool + require.True(t, errors.Is(mp.Add(r2.FallbackTransaction, fs, r2), ErrDup)) + + // good, higher priority than r2. The resulting mp.verifiedTxes: [r3, r2] + r3 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, smallNetFee+1), + } + require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3)) + require.True(t, mp.ContainsKey(r3.FallbackTransaction.Hash())) + data, ok = mp.TryGetData(r3.FallbackTransaction.Hash()) + require.True(t, ok) + require.Equal(t, r3, data) + + // good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4] + r4 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, smallNetFee), + } + require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4)) + require.True(t, mp.ContainsKey(r4.FallbackTransaction.Hash())) + data, ok = mp.TryGetData(r4.FallbackTransaction.Hash()) + require.True(t, ok) + require.Equal(t, r4, data) + + // good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4, r5] + r5 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, smallNetFee), + } + require.NoError(t, mp.Add(r5.FallbackTransaction, fs, r5)) + require.True(t, mp.ContainsKey(r5.FallbackTransaction.Hash())) + data, ok = mp.TryGetData(r5.FallbackTransaction.Hash()) + require.True(t, ok) + require.Equal(t, r5, data) + + // and both r2's and r4's data should still be reachable + data, ok = mp.TryGetData(r2.FallbackTransaction.Hash()) + require.True(t, ok) + require.Equal(t, r2, data) + data, ok = mp.TryGetData(r4.FallbackTransaction.Hash()) + require.True(t, ok) + require.Equal(t, r4, data) + + // should fail to get unexisting data + _, ok = mp.TryGetData(util.Uint256{0, 0, 0}) + require.False(t, ok) + + // but getting nil data is OK. The resulting mp.verifiedTxes: [r3, r2, r4, r5, r6] + r6 := newTx(t, smallNetFee) + require.NoError(t, mp.Add(r6, fs, nil)) + require.True(t, mp.ContainsKey(r6.Hash())) + data, ok = mp.TryGetData(r6.Hash()) + require.True(t, ok) + require.Nil(t, data) + + // getting data: item is in verifiedMap, but not in verifiedTxes + r7 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, smallNetFee), + } + require.NoError(t, mp.Add(r7.FallbackTransaction, fs, r4)) + require.True(t, mp.ContainsKey(r7.FallbackTransaction.Hash())) + r8 := &payload.P2PNotaryRequest{ + MainTransaction: newTx(t, 0), + FallbackTransaction: newTx(t, smallNetFee-1), + } + require.NoError(t, mp.Add(r8.FallbackTransaction, fs, r4)) + require.True(t, mp.ContainsKey(r8.FallbackTransaction.Hash())) + mp.verifiedTxes = append(mp.verifiedTxes[:len(mp.verifiedTxes)-2], mp.verifiedTxes[len(mp.verifiedTxes)-1]) + _, ok = mp.TryGetData(r7.FallbackTransaction.Hash()) + require.False(t, ok) +} diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index d8e1d66f8..e325a67a9 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "sync" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -26,17 +27,29 @@ type Notary struct { interop.ContractMD GAS *GAS Desig *Designate + + lock sync.RWMutex + // isValid defies whether cached values were changed during the current + // consensus iteration. If false, these values will be updated after + // blockchain DAO persisting. If true, we can safely use cached values. + isValid bool + maxNotValidBeforeDelta uint32 } const ( notaryName = "Notary" notaryContractID = reservedContractID - 1 + // NotaryVerificationPrice is the price of `verify` Notary method. + NotaryVerificationPrice = 100_0000 // prefixDeposit is a prefix for storing Notary deposits. - prefixDeposit = 1 - defaultDepositDeltaTill = 5760 + prefixDeposit = 1 + defaultDepositDeltaTill = 5760 + defaultMaxNotValidBeforeDelta = 140 // 20 rounds for 7 validators, a little more than half an hour ) +var maxNotValidBeforeDeltaKey = []byte{10} + // newNotary returns Notary native contract. func newNotary() *Notary { n := &Notary{ContractMD: *interop.NewContractMD(notaryName)} @@ -76,6 +89,15 @@ func newNotary() *Notary { md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates) n.AddMethod(md, desc) + desc = newDescriptor("getMaxNotValidBeforeDelta", smartcontract.IntegerType) + md = newMethodAndPrice(n.getMaxNotValidBeforeDelta, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc) + + desc = newDescriptor("setMaxNotValidBeforeDelta", smartcontract.BoolType, + manifest.NewParameter("value", smartcontract.IntegerType)) + md = newMethodAndPrice(n.setMaxNotValidBeforeDelta, 300_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc) + desc = newDescriptor("onPersist", smartcontract.VoidType) md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) n.AddMethod(md, desc) @@ -94,6 +116,8 @@ func (n *Notary) Metadata() *interop.ContractMD { // Initialize initializes Notary native contract and implements Contract interface. func (n *Notary) Initialize(ic *interop.Context) error { + n.isValid = true + n.maxNotValidBeforeDelta = defaultMaxNotValidBeforeDelta return nil } @@ -116,7 +140,7 @@ func (n *Notary) OnPersist(ic *interop.Context) error { nFees += int64(nKeys) + 1 if tx.Sender() == n.Hash { payer := tx.Signers[1] - balance := n.getDepositFor(ic.DAO, payer.Account) + balance := n.GetDepositFor(ic.DAO, payer.Account) balance.Amount.Sub(balance.Amount, big.NewInt(tx.SystemFee+tx.NetworkFee)) if balance.Amount.Sign() == 0 { err := n.removeDepositFor(ic.DAO, payer.Account) @@ -142,6 +166,19 @@ func (n *Notary) OnPersist(ic *interop.Context) error { return nil } +// OnPersistEnd updates cached Policy values if they've been changed +func (n *Notary) OnPersistEnd(dao dao.DAO) error { + if n.isValid { + return nil + } + n.lock.Lock() + defer n.lock.Unlock() + + n.maxNotValidBeforeDelta = getUint32WithKey(n.ContractID, dao, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta) + n.isValid = true + return nil +} + // onPayment records deposited amount as belonging to "from" address with a lock // till the specified chain's height. func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -162,7 +199,7 @@ func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem allowedChangeTill := ic.Tx.Sender() == to currentHeight := ic.Chain.BlockHeight() - deposit := n.getDepositFor(ic.DAO, to) + deposit := n.GetDepositFor(ic.DAO, to) till := toUint32(additionalParams[1]) if till < currentHeight { panic(fmt.Errorf("`till` shouldn't be less then the chain's height %d", currentHeight)) @@ -206,7 +243,7 @@ func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) st if till < ic.Chain.BlockHeight() { return stackitem.NewBool(false) } - deposit := n.getDepositFor(ic.DAO, addr) + deposit := n.GetDepositFor(ic.DAO, addr) if deposit == nil { return stackitem.NewBool(false) } @@ -235,7 +272,7 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem. if !args[1].Equals(stackitem.Null{}) { to = toUint160(args[1]) } - deposit := n.getDepositFor(ic.DAO, from) + deposit := n.GetDepositFor(ic.DAO, from) if deposit == nil { return stackitem.NewBool(false) } @@ -263,21 +300,31 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem. // balanceOf returns deposited GAS amount for specified address. func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { acc := toUint160(args[0]) - deposit := n.getDepositFor(ic.DAO, acc) + return stackitem.NewBigInteger(n.BalanceOf(ic.DAO, acc)) +} + +// BalanceOf is an internal representation of `balanceOf` Notary method. +func (n *Notary) BalanceOf(dao dao.DAO, acc util.Uint160) *big.Int { + deposit := n.GetDepositFor(dao, acc) if deposit == nil { - return stackitem.NewBigInteger(big.NewInt(0)) + return big.NewInt(0) } - return stackitem.NewBigInteger(deposit.Amount) + return deposit.Amount } // expirationOf Returns deposit lock height for specified address. func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { acc := toUint160(args[0]) - deposit := n.getDepositFor(ic.DAO, acc) + return stackitem.Make(n.ExpirationOf(ic.DAO, acc)) +} + +// ExpirationOf is an internal representation of `expirationOf` Notary method. +func (n *Notary) ExpirationOf(dao dao.DAO, acc util.Uint160) uint32 { + deposit := n.GetDepositFor(dao, acc) if deposit == nil { - return stackitem.Make(0) + return 0 } - return stackitem.Make(deposit.Till) + return deposit.Till } // verify checks whether the transaction was signed by one of the notaries. @@ -302,7 +349,7 @@ func (n *Notary) verify(ic *interop.Context, args []stackitem.Item) stackitem.It return stackitem.NewBool(false) } payer := tx.Signers[1].Account - balance := n.getDepositFor(ic.DAO, payer) + balance := n.GetDepositFor(ic.DAO, payer) if balance == nil || balance.Amount.Cmp(big.NewInt(tx.NetworkFee+tx.SystemFee)) < 0 { return stackitem.NewBool(false) } @@ -328,9 +375,47 @@ func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) { return nodes, err } -// getDepositFor returns state.Deposit for the account specified. It returns nil in case if +// getMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. +func (n *Notary) getMaxNotValidBeforeDelta(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(big.NewInt(int64(n.GetMaxNotValidBeforeDelta(ic.DAO)))) +} + +// GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method. +func (n *Notary) GetMaxNotValidBeforeDelta(dao dao.DAO) uint32 { + n.lock.RLock() + defer n.lock.RUnlock() + if n.isValid { + return n.maxNotValidBeforeDelta + } + return getUint32WithKey(n.ContractID, dao, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta) +} + +// setMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. +func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem.Item) stackitem.Item { + value := toUint32(args[0]) + if value > transaction.MaxValidUntilBlockIncrement/2 || value < uint32(ic.Chain.GetConfig().ValidatorsCount) { + panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", transaction.MaxValidUntilBlockIncrement/2, ic.Chain.GetConfig().ValidatorsCount)) + } + ok, err := checkValidators(ic) + if err != nil { + panic(fmt.Errorf("failed to check committee signature: %w", err)) + } + if !ok { + return stackitem.NewBool(false) + } + n.lock.Lock() + defer n.lock.Unlock() + err = setUint32WithKey(n.ContractID, ic.DAO, maxNotValidBeforeDeltaKey, value) + if err != nil { + panic(fmt.Errorf("failed to put value into the storage: %w", err)) + } + n.isValid = false + return stackitem.NewBool(true) +} + +// GetDepositFor returns state.Deposit for the account specified. It returns nil in case if // deposit is not found in storage and panics in case of any other error. -func (n *Notary) getDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit { +func (n *Notary) GetDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit { key := append([]byte{prefixDeposit}, acc.BytesBE()...) deposit := new(state.Deposit) err := getSerializableFromDAO(n.ContractID, dao, key, deposit) diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index f9d38e088..3d9c57cb2 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -1,7 +1,6 @@ package native import ( - "encoding/binary" "fmt" "math/big" "sort" @@ -10,7 +9,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" - "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "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/network/payload" @@ -167,10 +165,10 @@ func (p *Policy) OnPersistEnd(dao dao.DAO) error { p.lock.Lock() defer p.lock.Unlock() - p.maxTransactionsPerBlock = p.getUint32WithKey(dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock) - p.maxBlockSize = p.getUint32WithKey(dao, maxBlockSizeKey, defaultMaxBlockSize) - p.feePerByte = p.getInt64WithKey(dao, feePerByteKey, defaultFeePerByte) - p.maxBlockSystemFee = p.getInt64WithKey(dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) + p.maxTransactionsPerBlock = getUint32WithKey(p.ContractID, dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock) + p.maxBlockSize = getUint32WithKey(p.ContractID, dao, maxBlockSizeKey, defaultMaxBlockSize) + p.feePerByte = getInt64WithKey(p.ContractID, dao, feePerByteKey, defaultFeePerByte) + p.maxBlockSystemFee = getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) p.maxVerificationGas = defaultMaxVerificationGas p.blockedAccounts = make([]util.Uint160, 0) @@ -207,7 +205,7 @@ func (p *Policy) GetMaxTransactionsPerBlockInternal(dao dao.DAO) uint32 { if p.isValid { return p.maxTransactionsPerBlock } - return p.getUint32WithKey(dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock) + return getUint32WithKey(p.ContractID, dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock) } // getMaxBlockSize is Policy contract method and returns maximum block size. @@ -222,7 +220,7 @@ func (p *Policy) GetMaxBlockSizeInternal(dao dao.DAO) uint32 { if p.isValid { return p.maxBlockSize } - return p.getUint32WithKey(dao, maxBlockSizeKey, defaultMaxBlockSize) + return getUint32WithKey(p.ContractID, dao, maxBlockSizeKey, defaultMaxBlockSize) } // getFeePerByte is Policy contract method and returns required transaction's fee @@ -238,7 +236,7 @@ func (p *Policy) GetFeePerByteInternal(dao dao.DAO) int64 { if p.isValid { return p.feePerByte } - return p.getInt64WithKey(dao, feePerByteKey, defaultFeePerByte) + return getInt64WithKey(p.ContractID, dao, feePerByteKey, defaultFeePerByte) } // GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion. @@ -262,7 +260,7 @@ func (p *Policy) GetMaxBlockSystemFeeInternal(dao dao.DAO) int64 { if p.isValid { return p.maxBlockSystemFee } - return p.getInt64WithKey(dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) + return getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) } // isBlocked is Policy contract method and checks whether provided account is blocked. @@ -296,7 +294,7 @@ func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackite if value > block.MaxTransactionsPerBlock { panic(fmt.Errorf("MaxTransactionsPerBlock cannot exceed the maximum allowed transactions per block = %d", block.MaxTransactionsPerBlock)) } - ok, err := p.checkValidators(ic) + ok, err := checkValidators(ic) if err != nil { panic(err) } @@ -305,7 +303,7 @@ func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackite } p.lock.Lock() defer p.lock.Unlock() - err = p.setUint32WithKey(ic.DAO, maxTransactionsPerBlockKey, value) + err = setUint32WithKey(p.ContractID, ic.DAO, maxTransactionsPerBlockKey, value) if err != nil { panic(err) } @@ -319,7 +317,7 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta if value > payload.MaxSize { panic(fmt.Errorf("MaxBlockSize cannot be more than the maximum payload size = %d", payload.MaxSize)) } - ok, err := p.checkValidators(ic) + ok, err := checkValidators(ic) if err != nil { panic(err) } @@ -328,7 +326,7 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta } p.lock.Lock() defer p.lock.Unlock() - err = p.setUint32WithKey(ic.DAO, maxBlockSizeKey, value) + err = setUint32WithKey(p.ContractID, ic.DAO, maxBlockSizeKey, value) if err != nil { panic(err) } @@ -342,7 +340,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack if value < 0 || value > maxFeePerByte { panic(fmt.Errorf("FeePerByte shouldn't be negative or greater than %d", maxFeePerByte)) } - ok, err := p.checkValidators(ic) + ok, err := checkValidators(ic) if err != nil { panic(err) } @@ -351,7 +349,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack } p.lock.Lock() defer p.lock.Unlock() - err = p.setInt64WithKey(ic.DAO, feePerByteKey, value) + err = setInt64WithKey(p.ContractID, ic.DAO, feePerByteKey, value) if err != nil { panic(err) } @@ -365,7 +363,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item if value <= minBlockSystemFee { panic(fmt.Errorf("MaxBlockSystemFee cannot be less then %d", minBlockSystemFee)) } - ok, err := p.checkValidators(ic) + ok, err := checkValidators(ic) if err != nil { panic(err) } @@ -374,7 +372,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item } p.lock.Lock() defer p.lock.Unlock() - err = p.setInt64WithKey(ic.DAO, maxBlockSystemFeeKey, value) + err = setInt64WithKey(p.ContractID, ic.DAO, maxBlockSystemFeeKey, value) if err != nil { panic(err) } @@ -385,7 +383,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item // blockAccount is Policy contract method and adds given account hash to the list // of blocked accounts. func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - ok, err := p.checkValidators(ic) + ok, err := checkValidators(ic) if err != nil { panic(err) } @@ -412,7 +410,7 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki // unblockAccount is Policy contract method and removes given account hash from // the list of blocked accounts. func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - ok, err := p.checkValidators(ic) + ok, err := checkValidators(ic) if err != nil { panic(err) } @@ -434,46 +432,6 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac return stackitem.NewBool(true) } -func (p *Policy) getUint32WithKey(dao dao.DAO, key []byte, defaultValue uint32) uint32 { - si := dao.GetStorageItem(p.ContractID, key) - if si == nil { - return defaultValue - } - return binary.LittleEndian.Uint32(si.Value) -} - -func (p *Policy) setUint32WithKey(dao dao.DAO, key []byte, value uint32) error { - si := &state.StorageItem{ - Value: make([]byte, 4), - } - binary.LittleEndian.PutUint32(si.Value, value) - return dao.PutStorageItem(p.ContractID, key, si) -} - -func (p *Policy) getInt64WithKey(dao dao.DAO, key []byte, defaultValue int64) int64 { - si := dao.GetStorageItem(p.ContractID, key) - if si == nil { - return defaultValue - } - return int64(binary.LittleEndian.Uint64(si.Value)) -} - -func (p *Policy) setInt64WithKey(dao dao.DAO, key []byte, value int64) error { - si := &state.StorageItem{ - Value: make([]byte, 8), - } - binary.LittleEndian.PutUint64(si.Value, uint64(value)) - return dao.PutStorageItem(p.ContractID, key, si) -} - -func (p *Policy) checkValidators(ic *interop.Context) (bool, error) { - prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash) - if err != nil { - return false, err - } - return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus) -} - // CheckPolicy checks whether transaction conforms to current policy restrictions // like not being signed by blocked account or not exceeding block-level system // fee limit. diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go index 2ca752105..38a17e58a 100644 --- a/pkg/core/native/util.go +++ b/pkg/core/native/util.go @@ -1,7 +1,11 @@ package native import ( + "encoding/binary" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/io" @@ -27,3 +31,43 @@ func putSerializableToDAO(id int32, d dao.DAO, key []byte, item io.Serializable) Value: w.Bytes(), }) } + +func getInt64WithKey(id int32, d dao.DAO, key []byte, defaultValue int64) int64 { + si := d.GetStorageItem(id, key) + if si == nil { + return defaultValue + } + return int64(binary.LittleEndian.Uint64(si.Value)) +} + +func setInt64WithKey(id int32, dao dao.DAO, key []byte, value int64) error { + si := &state.StorageItem{ + Value: make([]byte, 8), + } + binary.LittleEndian.PutUint64(si.Value, uint64(value)) + return dao.PutStorageItem(id, key, si) +} + +func getUint32WithKey(id int32, dao dao.DAO, key []byte, defaultValue uint32) uint32 { + si := dao.GetStorageItem(id, key) + if si == nil { + return defaultValue + } + return binary.LittleEndian.Uint32(si.Value) +} + +func setUint32WithKey(id int32, dao dao.DAO, key []byte, value uint32) error { + si := &state.StorageItem{ + Value: make([]byte, 4), + } + binary.LittleEndian.PutUint32(si.Value, value) + return dao.PutStorageItem(id, key, si) +} + +func checkValidators(ic *interop.Context) (bool, error) { + prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash) + if err != nil { + return false, err + } + return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus) +} diff --git a/pkg/core/native_notary_test.go b/pkg/core/native_notary_test.go index 2a67bc423..a9d2ff614 100644 --- a/pkg/core/native_notary_test.go +++ b/pkg/core/native_notary_test.go @@ -2,12 +2,14 @@ package core import ( "math" + "math/big" "testing" "github.com/nspcc-dev/neo-go/internal/testchain" "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/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -319,3 +321,48 @@ func TestNotaryNodesReward(t *testing.T) { checkReward(5, 7, spendDeposit) } } + +func TestMaxNotValidBeforeDelta(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + notaryHash := chain.contracts.Notary.Hash + + t.Run("get, internal method", func(t *testing.T) { + n := chain.contracts.Notary.GetMaxNotValidBeforeDelta(chain.dao) + require.Equal(t, 140, int(n)) + }) + + t.Run("get, contract method", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, notaryHash, "getMaxNotValidBeforeDelta") + require.NoError(t, err) + checkResult(t, res, stackitem.NewBigInteger(big.NewInt(140))) + require.NoError(t, chain.persist()) + }) + + t.Run("set", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(150))) + require.NoError(t, err) + checkResult(t, res, stackitem.NewBool(true)) + n := chain.contracts.Notary.GetMaxNotValidBeforeDelta(chain.dao) + require.Equal(t, 150, int(n)) + }) + + t.Run("set, too big value", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(transaction.MaxValidUntilBlockIncrement/2+1))) + require.NoError(t, err) + checkFAULTState(t, res) + }) + + t.Run("set, too small value", func(t *testing.T) { + res, err := invokeContractMethod(chain, 100000000, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(int64(chain.GetConfig().ValidatorsCount-1)))) + require.NoError(t, err) + checkFAULTState(t, res) + }) + + t.Run("set, not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(150))) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) +} diff --git a/pkg/core/native_policy_test.go b/pkg/core/native_policy_test.go index b1fb38eca..8ecfd0a2a 100644 --- a/pkg/core/native_policy_test.go +++ b/pkg/core/native_policy_test.go @@ -11,6 +11,7 @@ import ( "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/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -45,6 +46,13 @@ func TestMaxTransactionsPerBlock(t *testing.T) { require.NoError(t, err) checkFAULTState(t, res) }) + + t.Run("set, not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024))) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) } func TestMaxBlockSize(t *testing.T) { @@ -80,6 +88,13 @@ func TestMaxBlockSize(t *testing.T) { require.NoError(t, err) checkFAULTState(t, res) }) + + t.Run("set, not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400))) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) } func TestFeePerByte(t *testing.T) { @@ -119,6 +134,13 @@ func TestFeePerByte(t *testing.T) { require.NoError(t, err) checkFAULTState(t, res) }) + + t.Run("set, not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(1024))) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) } func TestBlockSystemFee(t *testing.T) { @@ -154,6 +176,13 @@ func TestBlockSystemFee(t *testing.T) { checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000))) require.NoError(t, chain.persist()) }) + + t.Run("set, not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000))) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) } func TestBlockedAccounts(t *testing.T) { @@ -217,4 +246,11 @@ func TestBlockedAccounts(t *testing.T) { checkResult(t, res, stackitem.NewBool(false)) require.NoError(t, chain.persist()) }) + + t.Run("not signed by committee", func(t *testing.T) { + signer, err := wallet.NewAccount() + require.NoError(t, err) + invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "blockAccount", account.BytesBE()) + checkResult(t, invokeRes, stackitem.NewBool(false)) + }) } diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 894a5f03a..346266f62 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -13,6 +13,7 @@ import ( "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/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -31,21 +32,30 @@ import ( type testChain struct { config.ProtocolConfiguration *mempool.Pool - blocksCh []chan<- *block.Block - blockheight uint32 - poolTx func(*transaction.Transaction) error - blocks map[util.Uint256]*block.Block - hdrHashes map[uint32]util.Uint256 - txs map[util.Uint256]*transaction.Transaction + 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), - poolTx: func(*transaction.Transaction) error { return nil }, - blocks: make(map[util.Uint256]*block.Block), - hdrHashes: make(map[uint32]util.Uint256), - txs: make(map[util.Uint256]*transaction.Transaction), + 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}, } } @@ -65,6 +75,48 @@ func (chain *testChain) putTx(tx *transaction.Transaction) { 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 (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) 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 } @@ -77,7 +129,7 @@ func (chain *testChain) FeePerByte() int64 { } func (chain *testChain) P2PSigExtensionsEnabled() bool { - return false + return true } func (chain *testChain) GetMaxBlockSystemFee() int64 { @@ -207,6 +259,9 @@ func (chain *testChain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, ui } func (chain *testChain) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int { + if chain.utilityTokenBalance != nil { + return chain.utilityTokenBalance + } panic("TODO") } @@ -230,7 +285,10 @@ func (chain *testChain) SubscribeForTransactions(ch chan<- *transaction.Transact func (chain *testChain) VerifyTx(*transaction.Transaction) error { panic("TODO") } -func (*testChain) VerifyWitness(util.Uint160, crypto.Verifiable, *transaction.Witness, int64) error { +func (chain *testChain) VerifyWitness(util.Uint160, crypto.Verifiable, *transaction.Witness, int64) error { + if chain.verifyWitnessF != nil { + return chain.verifyWitnessF() + } panic("TODO") } diff --git a/pkg/network/message.go b/pkg/network/message.go index f1904a7d3..3b73a5422 100644 --- a/pkg/network/message.go +++ b/pkg/network/message.go @@ -64,18 +64,19 @@ const ( CMDPong CommandType = 0x19 // synchronization - CMDGetHeaders CommandType = 0x20 - CMDHeaders CommandType = 0x21 - CMDGetBlocks CommandType = 0x24 - CMDMempool CommandType = 0x25 - CMDInv CommandType = 0x27 - CMDGetData CommandType = 0x28 - CMDGetBlockByIndex CommandType = 0x29 - CMDNotFound CommandType = 0x2a - CMDTX = CommandType(payload.TXType) - CMDBlock = CommandType(payload.BlockType) - CMDConsensus = CommandType(payload.ConsensusType) - CMDReject CommandType = 0x2f + CMDGetHeaders CommandType = 0x20 + CMDHeaders CommandType = 0x21 + CMDGetBlocks CommandType = 0x24 + CMDMempool CommandType = 0x25 + CMDInv CommandType = 0x27 + CMDGetData CommandType = 0x28 + CMDGetBlockByIndex CommandType = 0x29 + CMDNotFound CommandType = 0x2a + CMDTX = CommandType(payload.TXType) + CMDBlock = CommandType(payload.BlockType) + CMDConsensus = CommandType(payload.ConsensusType) + CMDP2PNotaryRequest = CommandType(payload.P2PNotaryRequestType) + CMDReject CommandType = 0x2f // SPV protocol CMDFilterLoad CommandType = 0x30 @@ -148,6 +149,8 @@ func (m *Message) decodePayload() error { p = block.New(m.Network, m.StateRootInHeader) case CMDConsensus: p = consensus.NewPayload(m.Network, m.StateRootInHeader) + case CMDP2PNotaryRequest: + p = &payload.P2PNotaryRequest{Network: m.Network} case CMDGetBlocks: p = &payload.GetBlocks{} case CMDGetHeaders: diff --git a/pkg/network/notary_feer.go b/pkg/network/notary_feer.go new file mode 100644 index 000000000..97e179234 --- /dev/null +++ b/pkg/network/notary_feer.go @@ -0,0 +1,40 @@ +package network + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// NotaryFeer implements mempool.Feer interface for Notary balance handling. +type NotaryFeer struct { + bc blockchainer.Blockchainer +} + +// FeePerByte implements mempool.Feer interface. +func (f NotaryFeer) FeePerByte() int64 { + return f.bc.FeePerByte() +} + +// GetUtilityTokenBalance implements mempool.Feer interface. +func (f NotaryFeer) GetUtilityTokenBalance(acc util.Uint160) *big.Int { + return f.bc.GetNotaryBalance(acc) +} + +// BlockHeight implements mempool.Feer interface. +func (f NotaryFeer) BlockHeight() uint32 { + return f.bc.BlockHeight() +} + +// P2PSigExtensionsEnabled implements mempool.Feer interface. +func (f NotaryFeer) P2PSigExtensionsEnabled() bool { + return f.bc.P2PSigExtensionsEnabled() +} + +// NewNotaryFeer returns new NotaryFeer instance. +func NewNotaryFeer(bc blockchainer.Blockchainer) NotaryFeer { + return NotaryFeer{ + bc: bc, + } +} diff --git a/pkg/network/payload/inventory.go b/pkg/network/payload/inventory.go index 988c40eb0..8727e2929 100644 --- a/pkg/network/payload/inventory.go +++ b/pkg/network/payload/inventory.go @@ -20,21 +20,24 @@ func (i InventoryType) String() string { return "block" case ConsensusType: return "consensus" + case P2PNotaryRequestType: + return "p2pNotaryRequest" default: return "unknown inventory type" } } // Valid returns true if the inventory (type) is known. -func (i InventoryType) Valid() bool { - return i == BlockType || i == TXType || i == ConsensusType +func (i InventoryType) Valid(p2pSigExtensionsEnabled bool) bool { + return i == BlockType || i == TXType || i == ConsensusType || (p2pSigExtensionsEnabled && i == P2PNotaryRequestType) } // List of valid InventoryTypes. const ( - TXType InventoryType = 0x2b - BlockType InventoryType = 0x2c - ConsensusType InventoryType = 0x2d + TXType InventoryType = 0x2b + BlockType InventoryType = 0x2c + ConsensusType InventoryType = 0x2d + P2PNotaryRequestType InventoryType = 0x50 ) // Inventory payload. diff --git a/pkg/network/payload/inventory_test.go b/pkg/network/payload/inventory_test.go index 6f359e9dc..7d3920f9e 100644 --- a/pkg/network/payload/inventory_test.go +++ b/pkg/network/payload/inventory_test.go @@ -31,15 +31,22 @@ func TestEmptyInv(t *testing.T) { } func TestValid(t *testing.T) { - require.True(t, TXType.Valid()) - require.True(t, BlockType.Valid()) - require.True(t, ConsensusType.Valid()) - require.False(t, InventoryType(0xFF).Valid()) + require.True(t, TXType.Valid(false)) + require.True(t, TXType.Valid(true)) + require.True(t, BlockType.Valid(false)) + require.True(t, BlockType.Valid(true)) + require.True(t, ConsensusType.Valid(false)) + require.True(t, ConsensusType.Valid(true)) + require.False(t, P2PNotaryRequestType.Valid(false)) + require.True(t, P2PNotaryRequestType.Valid(true)) + require.False(t, InventoryType(0xFF).Valid(false)) + require.False(t, InventoryType(0xFF).Valid(true)) } func TestString(t *testing.T) { require.Equal(t, "TX", TXType.String()) require.Equal(t, "block", BlockType.String()) require.Equal(t, "consensus", ConsensusType.String()) + require.Equal(t, "p2pNotaryRequest", P2PNotaryRequestType.String()) require.True(t, strings.Contains(InventoryType(0xFF).String(), "unknown")) } diff --git a/pkg/network/payload/notary_request.go b/pkg/network/payload/notary_request.go new file mode 100644 index 000000000..1a7f5e560 --- /dev/null +++ b/pkg/network/payload/notary_request.go @@ -0,0 +1,149 @@ +package payload + +import ( + "bytes" + "errors" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" +) + +// P2PNotaryRequest contains main and fallback transactions for the Notary service. +type P2PNotaryRequest struct { + MainTransaction *transaction.Transaction + FallbackTransaction *transaction.Transaction + Network netmode.Magic + + Witness transaction.Witness + + hash util.Uint256 + signedHash util.Uint256 +} + +// Hash returns payload's hash. +func (r *P2PNotaryRequest) Hash() util.Uint256 { + if r.hash.Equals(util.Uint256{}) { + if r.createHash() != nil { + panic("failed to compute hash!") + } + } + return r.hash +} + +// GetSignedHash returns a hash of the payload used to verify it. +func (r *P2PNotaryRequest) GetSignedHash() util.Uint256 { + if r.signedHash.Equals(util.Uint256{}) { + if r.createHash() != nil { + panic("failed to compute hash!") + } + } + return r.signedHash +} + +// GetSignedPart returns a part of the payload which must be signed. +func (r *P2PNotaryRequest) GetSignedPart() []byte { + buf := io.NewBufBinWriter() + buf.WriteU32LE(uint32(r.Network)) + r.encodeHashableFields(buf.BinWriter) + if buf.Err != nil { + return nil + } + return buf.Bytes() +} + +// createHash creates hash of the payload. +func (r *P2PNotaryRequest) createHash() error { + b := r.GetSignedPart() + if b == nil { + return errors.New("failed to serialize hashable data") + } + r.updateHashes(b) + return nil +} + +// updateHashes updates Payload's hashes based on the given buffer which should +// be a signable data slice. +func (r *P2PNotaryRequest) updateHashes(b []byte) { + r.signedHash = hash.Sha256(b) + r.hash = hash.Sha256(r.signedHash.BytesBE()) +} + +// DecodeBinaryUnsigned reads payload from w excluding signature. +func (r *P2PNotaryRequest) decodeHashableFields(br *io.BinReader) { + r.MainTransaction = new(transaction.Transaction) + r.FallbackTransaction = new(transaction.Transaction) + r.MainTransaction.DecodeBinary(br) + r.FallbackTransaction.DecodeBinary(br) + if br.Err == nil { + br.Err = r.isValid() + } + if br.Err == nil { + br.Err = r.createHash() + } +} + +// DecodeBinary implements io.Serializable interface. +func (r *P2PNotaryRequest) DecodeBinary(br *io.BinReader) { + r.decodeHashableFields(br) + if br.Err == nil { + r.Witness.DecodeBinary(br) + } +} + +// encodeHashableFields writes payload to w excluding signature. +func (r *P2PNotaryRequest) encodeHashableFields(bw *io.BinWriter) { + r.MainTransaction.EncodeBinary(bw) + r.FallbackTransaction.EncodeBinary(bw) +} + +// EncodeBinary implements Serializable interface. +func (r *P2PNotaryRequest) EncodeBinary(bw *io.BinWriter) { + r.encodeHashableFields(bw) + r.Witness.EncodeBinary(bw) +} + +func (r *P2PNotaryRequest) isValid() error { + nKeysMain := r.MainTransaction.GetAttributes(transaction.NotaryAssistedT) + if len(nKeysMain) == 0 { + return errors.New("main transaction should have NotaryAssisted attribute") + } + if nKeysMain[0].Value.(*transaction.NotaryAssisted).NKeys == 0 { + return errors.New("main transaction should have NKeys > 0") + } + if len(r.FallbackTransaction.Signers) != 2 { + return errors.New("fallback transaction should have two signers") + } + if len(r.FallbackTransaction.Scripts) != 2 { + return errors.New("fallback transaction should have dummy Notary witness and valid witness for the second signer") + } + if len(r.FallbackTransaction.Scripts[0].InvocationScript) != 66 || + len(r.FallbackTransaction.Scripts[0].VerificationScript) != 0 || + !bytes.HasPrefix(r.FallbackTransaction.Scripts[0].InvocationScript, []byte{byte(opcode.PUSHDATA1), 64}) { + return errors.New("fallback transaction has invalid dummy Notary witness") + } + if !r.FallbackTransaction.HasAttribute(transaction.NotValidBeforeT) { + return errors.New("fallback transactions should have NotValidBefore attribute") + } + conflicts := r.FallbackTransaction.GetAttributes(transaction.ConflictsT) + if len(conflicts) != 1 { + return errors.New("fallback transaction should have one Conflicts attribute") + } + if conflicts[0].Value.(*transaction.Conflicts).Hash != r.MainTransaction.Hash() { + return errors.New("fallback transaction does not conflicts with the main transaction") + } + nKeysFallback := r.FallbackTransaction.GetAttributes(transaction.NotaryAssistedT) + if len(nKeysFallback) == 0 { + return errors.New("fallback transaction should have NotaryAssisted attribute") + } + if nKeysFallback[0].Value.(*transaction.NotaryAssisted).NKeys != 0 { + return errors.New("fallback transaction should have NKeys = 0") + } + if r.MainTransaction.ValidUntilBlock != r.FallbackTransaction.ValidUntilBlock { + return errors.New("both main and fallback transactions should have the same ValidUntil value") + } + return nil +} diff --git a/pkg/network/payload/notary_request_test.go b/pkg/network/payload/notary_request_test.go new file mode 100644 index 000000000..fd204676b --- /dev/null +++ b/pkg/network/payload/notary_request_test.go @@ -0,0 +1,184 @@ +package payload + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/internal/testserdes" + "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/require" +) + +func TestNotaryRequestIsValid(t *testing.T) { + mainTx := &transaction.Transaction{ + Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}}, + Script: []byte{0, 1, 2}, + ValidUntilBlock: 123, + } + errorCases := map[string]*P2PNotaryRequest{ + "main tx: missing NotaryAssisted attribute": {MainTransaction: &transaction.Transaction{}}, + "main tx: zero NKeys": {MainTransaction: &transaction.Transaction{Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}}}}, + "fallback transaction: invalid signers count": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{Signers: []transaction.Signer{{Account: random.Uint160()}}}, + }, + "fallback transaction: invalid witnesses count": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{Signers: []transaction.Signer{{Account: random.Uint160()}}}, + }, + "fallback tx: invalid dummy Notary witness (bad witnesses length)": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{}}, + }, + }, + "fallback tx: invalid dummy Notary witness (bad invocation script length)": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{}, {}}, + }, + }, + "fallback tx: invalid dummy Notary witness (bad invocation script prefix)": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 65}, make([]byte, 64, 64)...)}, {}}, + }, + }, + "fallback tx: invalid dummy Notary witness (non-empty verification script))": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 1, 1)}, {}}, + }, + }, + "fallback tx: missing NotValidBefore attribute": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + }, + "fallback tx: invalid number of Conflicts attributes": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Attributes: []transaction.Attribute{{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}}, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + }, + "fallback tx: does not conflicts with main tx": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: util.Uint256{}}}, + }, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + }, + "fallback tx: missing NotaryAssisted attribute": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + }, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + }, + "fallback tx: non-zero NKeys": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}, + }, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + }, + "fallback tx: ValidUntilBlock mismatch": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + ValidUntilBlock: 321, + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}, + }, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + }, + } + for name, errCase := range errorCases { + t.Run(name, func(t *testing.T) { + require.Error(t, errCase.isValid()) + }) + } + t.Run("good", func(t *testing.T) { + p := &P2PNotaryRequest{ + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{ + ValidUntilBlock: 123, + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}, + }, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {}}, + }, + } + require.NoError(t, p.isValid()) + }) +} + +func TestNotaryRequestEncodeDecodeBinary(t *testing.T) { + mainTx := &transaction.Transaction{ + Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}}, + Script: []byte{0, 1, 2}, + ValidUntilBlock: 123, + Signers: []transaction.Signer{{Account: util.Uint160{1, 5, 9}}}, + Scripts: []transaction.Witness{{ + InvocationScript: []byte{1, 4, 7}, + VerificationScript: []byte{3, 6, 9}, + }}, + } + _ = mainTx.Hash() + _ = mainTx.Size() + fallbackTx := &transaction.Transaction{ + Script: []byte{3, 2, 1}, + ValidUntilBlock: 123, + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}, + }, + Signers: []transaction.Signer{{Account: util.Uint160{1, 4, 7}}, {Account: util.Uint160{9, 8, 7}}}, + Scripts: []transaction.Witness{ + {InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, + {InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}}, + } + _ = fallbackTx.Hash() + _ = fallbackTx.Size() + p := &P2PNotaryRequest{ + MainTransaction: mainTx, + FallbackTransaction: fallbackTx, + Witness: transaction.Witness{ + InvocationScript: []byte{1, 2, 3}, + VerificationScript: []byte{7, 8, 9}, + }, + } + require.Equal(t, hash.Sha256(p.GetSignedHash().BytesBE()), p.Hash()) + testserdes.EncodeDecodeBinary(t, p, new(P2PNotaryRequest)) +} diff --git a/pkg/network/server.go b/pkg/network/server.go index 7fb73769b..8beeea9d2 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "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/network/capability" "github.com/nspcc-dev/neo-go/pkg/network/payload" @@ -59,11 +60,13 @@ type ( // stateRootInHeader specifies if block header contain state root. stateRootInHeader bool - transport Transporter - discovery Discoverer - chain blockchainer.Blockchainer - bQueue *blockQueue - consensus consensus.Service + transport Transporter + discovery Discoverer + chain blockchainer.Blockchainer + bQueue *blockQueue + consensus consensus.Service + notaryRequestPool *mempool.Pool + NotaryFeer NotaryFeer lock sync.RWMutex peers map[Peer]bool @@ -124,6 +127,15 @@ func newServerFromConstructors(config ServerConfig, chain blockchainer.Blockchai log: log, transactions: make(chan *transaction.Transaction, 64), } + if chain.P2PSigExtensionsEnabled() { + s.NotaryFeer = NewNotaryFeer(chain) + s.notaryRequestPool = mempool.New(chain.GetConfig().P2PNotaryRequestPayloadPoolSize, 1) + 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.bQueue = newBlockQueue(maxBlockBatch, chain, log, func(b *block.Block) { if !s.consensusStarted.Load() { s.tryStartConsensus() @@ -188,7 +200,7 @@ func (s *Server) Start(errChan chan error) { zap.Uint32("headerHeight", s.chain.HeaderHeight())) s.tryStartConsensus() - s.initStaleTxMemPool() + s.initStaleMemPools() go s.broadcastTxLoop() go s.relayBlocksLoop() @@ -507,6 +519,9 @@ func (s *Server) handleInvCmd(p Peer, inv *payload.Inventory) error { cp := s.consensus.GetPayload(h) return cp != nil }, + payload.P2PNotaryRequestType: func(h util.Uint256) bool { + return s.notaryRequestPool.ContainsKey(h) + }, } if exists := typExists[inv.Type]; exists != nil { for _, hash := range inv.Hashes { @@ -573,6 +588,12 @@ func (s *Server) handleGetDataCmd(p Peer, inv *payload.Inventory) error { if cp := s.consensus.GetPayload(hash); cp != nil { msg = NewMessage(CMDConsensus, cp) } + case payload.P2PNotaryRequestType: + if nrp, ok := s.notaryRequestPool.TryGetData(hash); ok { // already have checked P2PSigExtEnabled + msg = NewMessage(CMDP2PNotaryRequest, nrp.(*payload.P2PNotaryRequest)) + } else { + notFound = append(notFound, hash) + } } if msg != nil { pkt, err := msg.Bytes() @@ -687,11 +708,62 @@ func (s *Server) handleTxCmd(tx *transaction.Transaction) error { // in the pool. if s.verifyAndPoolTX(tx) == RelaySucceed { s.consensus.OnTransaction(tx) - s.broadcastTX(tx) + s.broadcastTX(tx, nil) } return nil } +// handleP2PNotaryRequestCmd process received P2PNotaryRequest payload. +func (s *Server) handleP2PNotaryRequestCmd(r *payload.P2PNotaryRequest) error { + if !s.chain.P2PSigExtensionsEnabled() { + return errors.New("P2PNotaryRequestCMD was received, but P2PSignatureExtensions are disabled") + } + if s.verifyAndPoolNotaryRequest(r) == RelaySucceed { + s.broadcastP2PNotaryRequestPayload(nil, r) + } + return nil +} + +// 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 { + switch { + case errors.Is(err, core.ErrAlreadyExists): + return RelayAlreadyExists + case errors.Is(err, core.ErrOOM): + return RelayOutOfMemory + case errors.Is(err, core.ErrPolicy): + return RelayPolicyFail + default: + return RelayInvalid + } + } + return RelaySucceed +} + +// verifyNotaryRequest is a function for state-dependant P2PNotaryRequest payload verification which is executed before ordinary blockchain's verification. +func verifyNotaryRequest(bc blockchainer.Blockchainer, _ *transaction.Transaction, data interface{}) error { + r := data.(*payload.P2PNotaryRequest) + payer := r.FallbackTransaction.Signers[1].Account + 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() { + return errors.New("P2PNotary contract should be a sender of the fallback transaction") + } + depositExpiration := bc.GetNotaryDepositExpiration(payer) + if r.FallbackTransaction.ValidUntilBlock >= depositExpiration { + return fmt.Errorf("fallback transaction is valid after deposit is unlocked: ValidUntilBlock is %d, deposit lock expires at %d", r.FallbackTransaction.ValidUntilBlock, depositExpiration) + } + return nil +} + +func (s *Server) broadcastP2PNotaryRequestPayload(_ *transaction.Transaction, data interface{}) { + r := data.(payload.P2PNotaryRequest) // we can guarantee that cast is successful + msg := NewMessage(CMDInv, payload.NewInventory(payload.P2PNotaryRequestType, []util.Uint256{r.FallbackTransaction.Hash()})) + s.broadcastMessage(msg) +} + // handleAddrCmd will process received addresses. func (s *Server) handleAddrCmd(p Peer, addrs *payload.AddressList) error { if !p.CanProcessAddr() { @@ -770,7 +842,7 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { if peer.Handshaked() { if inv, ok := msg.Payload.(*payload.Inventory); ok { - if !inv.Type.Valid() || len(inv.Hashes) == 0 { + if !inv.Type.Valid(s.chain.P2PSigExtensionsEnabled()) || len(inv.Hashes) == 0 { return errInvalidInvType } } @@ -808,6 +880,9 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error { case CMDTX: tx := msg.Payload.(*transaction.Transaction) return s.handleTxCmd(tx) + case CMDP2PNotaryRequest: + r := msg.Payload.(*payload.P2PNotaryRequest) + return s.handleP2PNotaryRequestCmd(r) case CMDPing: ping := msg.Payload.(*payload.Ping) return s.handlePing(peer, ping) @@ -943,13 +1018,13 @@ func (s *Server) verifyAndPoolTX(t *transaction.Transaction) RelayReason { func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason { ret := s.verifyAndPoolTX(t) if ret == RelaySucceed { - s.broadcastTX(t) + s.broadcastTX(t, nil) } return ret } // broadcastTX broadcasts an inventory message about new transaction. -func (s *Server) broadcastTX(t *transaction.Transaction) { +func (s *Server) broadcastTX(t *transaction.Transaction, _ interface{}) { select { case s.transactions <- t: case <-s.quit: @@ -964,8 +1039,8 @@ func (s *Server) broadcastTxHashes(hs []util.Uint256) { s.iteratePeersWithSendMsg(msg, Peer.EnqueuePacket, Peer.IsFullNode) } -// initStaleTxMemPool initializes mempool for stale tx processing. -func (s *Server) initStaleTxMemPool() { +// initStaleMemPools initializes mempools for stale tx/payload processing. +func (s *Server) initStaleMemPools() { cfg := s.chain.GetConfig() threshold := 5 if cfg.ValidatorsCount*2 > threshold { @@ -974,6 +1049,9 @@ func (s *Server) initStaleTxMemPool() { mp := s.chain.GetMemPool() mp.SetResendThreshold(uint32(threshold), s.broadcastTX) + if s.chain.P2PSigExtensionsEnabled() { + s.notaryRequestPool.SetResendThreshold(uint32(threshold), s.broadcastP2PNotaryRequestPayload) + } } // broadcastTxLoop is a loop for batching and sending diff --git a/pkg/network/server_test.go b/pkg/network/server_test.go index de4321fd9..2926b78fc 100644 --- a/pkg/network/server_test.go +++ b/pkg/network/server_test.go @@ -19,6 +19,8 @@ import ( "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/atomic" @@ -445,7 +447,7 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType, p.handshaked = true p.messageHandler = func(t *testing.T, msg *Message) { switch msg.Command { - case CMDTX, CMDBlock, CMDConsensus: + case CMDTX, CMDBlock, CMDConsensus, CMDP2PNotaryRequest: require.Equal(t, found, msg.Payload) recvResponse.Store(true) case CMDNotFound: @@ -463,6 +465,7 @@ 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) t.Run("block", func(t *testing.T) { b := newDummyBlock(2, 0) @@ -478,6 +481,44 @@ func TestGetData(t *testing.T) { notFound := []util.Uint256{hs[0], hs[2]} s.testHandleGetData(t, payload.TXType, hs, notFound, tx) }) + t.Run("p2pNotaryRequest", func(t *testing.T) { + mainTx := &transaction.Transaction{ + Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}}, + Script: []byte{0, 1, 2}, + ValidUntilBlock: 123, + Signers: []transaction.Signer{{Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}}, + } + mainTx.Size() + mainTx.Hash() + fallbackTx := &transaction.Transaction{ + Script: []byte{1, 2, 3}, + ValidUntilBlock: 123, + Attributes: []transaction.Attribute{ + {Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}, + {Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}}, + {Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}, + }, + Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}, + Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {InvocationScript: []byte{}, VerificationScript: []byte{}}}, + } + fallbackTx.Size() + fallbackTx.Hash() + r := &payload.P2PNotaryRequest{ + MainTransaction: mainTx, + FallbackTransaction: fallbackTx, + Network: netmode.UnitTestNet, + Witness: transaction.Witness{ + InvocationScript: []byte{1, 2, 3}, + VerificationScript: []byte{1, 2, 3}, + }, + } + r.Hash() + require.NoError(t, s.notaryRequestPool.Add(r.FallbackTransaction, s.chain, r)) + hs := []util.Uint256{random.Uint256(), r.FallbackTransaction.Hash(), random.Uint256()} + notFound := []util.Uint256{hs[0], hs[2]} + s.testHandleGetData(t, payload.P2PNotaryRequestType, hs, notFound, r) + }) } func initGetBlocksTest(t *testing.T) (*Server, func(), []*block.Block) { @@ -602,6 +643,7 @@ func TestGetHeaders(t *testing.T) { func TestInv(t *testing.T) { s, shutdown := startTestServer(t) defer shutdown() + s.chain.(*testChain).utilityTokenBalance = big.NewInt(10000000) var actual []util.Uint256 p := newLocalPeer(t, s) @@ -632,6 +674,24 @@ func TestInv(t *testing.T) { }) require.Equal(t, []util.Uint256{hs[0], hs[2]}, actual) }) + t.Run("p2pNotaryRequest", func(t *testing.T) { + fallbackTx := transaction.New(netmode.UnitTestNet, random.Bytes(100), 123) + fallbackTx.Signers = []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}} + fallbackTx.Size() + fallbackTx.Hash() + r := &payload.P2PNotaryRequest{ + MainTransaction: newDummyTx(), + FallbackTransaction: fallbackTx, + Network: netmode.UnitTestNet, + } + require.NoError(t, s.notaryRequestPool.Add(r.FallbackTransaction, s.chain, r)) + hs := []util.Uint256{random.Uint256(), r.FallbackTransaction.Hash(), random.Uint256()} + s.testHandleMessage(t, p, CMDInv, &payload.Inventory{ + Type: payload.P2PNotaryRequestType, + Hashes: hs, + }) + require.Equal(t, []util.Uint256{hs[0], hs[2]}, actual) + }) } func TestRequestTx(t *testing.T) { @@ -763,3 +823,43 @@ func TestMemPool(t *testing.T) { s.testHandleMessage(t, p, CMDMempool, payload.NullPayload{}) require.ElementsMatch(t, expected, actual) } + +func TestVerifyNotaryRequest(t *testing.T) { + bc := newTestChain() + 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()}}, + }, + Witness: transaction.Witness{}, + } + } + + t.Run("bad payload witness", func(t *testing.T) { + 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 } + r := newNotaryRequest() + r.FallbackTransaction.Signers[0] = transaction.Signer{Account: util.Uint160{7, 8, 9}} + require.Error(t, verifyNotaryRequest(bc, nil, r)) + }) + + t.Run("expired deposit", func(t *testing.T) { + r := newNotaryRequest() + 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 + require.NoError(t, verifyNotaryRequest(bc, nil, r)) + }) +}