Merge pull request #1582 from nspcc-dev/signature_collection/notary_request_payload

network: add notary request payload
This commit is contained in:
Roman Khimov 2020-12-10 18:51:09 +03:00 committed by GitHub
commit 3bbf7642ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1407 additions and 235 deletions

View file

@ -9,6 +9,9 @@ type (
ProtocolConfiguration struct { ProtocolConfiguration struct {
Magic netmode.Magic `yaml:"Magic"` Magic netmode.Magic `yaml:"Magic"`
MemPoolSize int `yaml:"MemPoolSize"` 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. // KeepOnlyLatestState specifies if MPT should only store latest state.
// If true, DB size will be smaller, but older roots won't be accessible. // If true, DB size will be smaller, but older roots won't be accessible.
// This value should remain the same for the same database. // This value should remain the same for the same database.
@ -17,7 +20,7 @@ type (
RemoveUntraceableBlocks bool `yaml:"RemoveUntraceableBlocks"` RemoveUntraceableBlocks bool `yaml:"RemoveUntraceableBlocks"`
// MaxTraceableBlocks is the length of the chain accessible to smart contracts. // MaxTraceableBlocks is the length of the chain accessible to smart contracts.
MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"` MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"`
// P2PSigExtensions enables additional signature-related transaction attributes // P2PSigExtensions enables additional signature-related logic.
P2PSigExtensions bool `yaml:"P2PSigExtensions"` P2PSigExtensions bool `yaml:"P2PSigExtensions"`
// ReservedAttributes allows to have reserved attributes range for experimental or private purposes. // ReservedAttributes allows to have reserved attributes range for experimental or private purposes.
ReservedAttributes bool `yaml:"ReservedAttributes"` ReservedAttributes bool `yaml:"ReservedAttributes"`

View file

@ -418,7 +418,7 @@ func (s *service) verifyBlock(b block.Block) bool {
s.log.Warn("proposed block has already outdated") s.log.Warn("proposed block has already outdated")
return false return false
} }
maxBlockSize := int(s.Chain.GetMaxBlockSize()) maxBlockSize := int(s.Chain.GetPolicer().GetMaxBlockSize())
size := io.GetVarSize(coreb) size := io.GetVarSize(coreb)
if size > maxBlockSize { if size > maxBlockSize {
s.log.Warn("proposed block size exceeds policy max block size", 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 fee int64
var pool = mempool.New(len(coreb.Transactions)) var pool = mempool.New(len(coreb.Transactions), 0)
var mainPool = s.Chain.GetMemPool() var mainPool = s.Chain.GetMemPool()
for _, tx := range coreb.Transactions { for _, tx := range coreb.Transactions {
var err error 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 { if fee > maxBlockSysFee {
s.log.Warn("proposed block system fee exceeds policy max block system fee", s.log.Warn("proposed block system fee exceeds policy max block system fee",
zap.Int("max system fee allowed", int(maxBlockSysFee)), zap.Int("max system fee allowed", int(maxBlockSysFee)),

View file

@ -386,7 +386,7 @@ func TestVerifyBlock(t *testing.T) {
require.False(t, srv.verifyBlock(&neoBlock{Block: *b})) require.False(t, srv.verifyBlock(&neoBlock{Block: *b}))
}) })
t.Run("bad big size", func(t *testing.T) { 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) script[0] = byte(opcode.RET)
tx := transaction.New(netmode.UnitTestNet, script, 100000) tx := transaction.New(netmode.UnitTestNet, script, 100000)
tx.ValidUntilBlock = 1 tx.ValidUntilBlock = 1
@ -407,7 +407,7 @@ func TestVerifyBlock(t *testing.T) {
t.Run("bad big sys fee", func(t *testing.T) { t.Run("bad big sys fee", func(t *testing.T) {
txes := make([]*transaction.Transaction, 2) txes := make([]*transaction.Transaction, 2)
for i := range txes { 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 txes[i].ValidUntilBlock = 1
addSender(t, txes[i]) addSender(t, txes[i])
signTx(t, srv.Chain.FeePerByte(), txes[i]) signTx(t, srv.Chain.FeePerByte(), txes[i])

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config" "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/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/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/mempool" "github.com/nspcc-dev/neo-go/pkg/core/mempool"
@ -42,9 +43,10 @@ const (
headerBatchCount = 2000 headerBatchCount = 2000
version = "0.1.0" version = "0.1.0"
defaultMemPoolSize = 50000 defaultMemPoolSize = 50000
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks defaultP2PNotaryRequestPayloadPoolSize = 1000
verificationGasLimit = 100000000 // 1 GAS defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
verificationGasLimit = 100000000 // 1 GAS
) )
var ( var (
@ -116,6 +118,10 @@ type Blockchain struct {
memPool *mempool.Pool 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 sbCommittee keys.PublicKeys
log *zap.Logger log *zap.Logger
@ -151,6 +157,10 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
cfg.MemPoolSize = defaultMemPoolSize cfg.MemPoolSize = defaultMemPoolSize
log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize)) 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 { if cfg.MaxTraceableBlocks == 0 {
cfg.MaxTraceableBlocks = defaultMaxTraceableBlocks cfg.MaxTraceableBlocks = defaultMaxTraceableBlocks
log.Info("MaxTraceableBlocks is not set or wrong, using default value", zap.Uint32("MaxTraceableBlocks", cfg.MaxTraceableBlocks)) 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), dao: dao.NewSimple(s, cfg.Magic, cfg.StateRootInHeader),
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
runToExitCh: make(chan struct{}), runToExitCh: make(chan struct{}),
memPool: mempool.New(cfg.MemPoolSize), memPool: mempool.New(cfg.MemPoolSize, 0),
sbCommittee: committee, sbCommittee: committee,
log: log, log: log,
events: make(chan bcEvent), events: make(chan bcEvent),
@ -452,7 +462,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error {
if !block.MerkleRoot.Equals(merkle) { if !block.MerkleRoot.Equals(merkle) {
return errors.New("invalid block: MerkleRoot mismatch") 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 { for _, tx := range block.Transactions {
var err error var err error
// Transactions are verified before adding them // Transactions are verified before adding them
@ -464,7 +474,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error {
continue continue
} }
} else { } else {
err = bc.verifyAndPoolTx(tx, mp) err = bc.verifyAndPoolTx(tx, mp, bc)
} }
if err != nil && bc.config.VerifyTransactions { if err != nil && bc.config.VerifyTransactions {
return fmt.Errorf("transaction %s failed to verify: %w", tx.Hash().StringLE(), err) 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() bc.lock.Unlock()
return fmt.Errorf("failed to call OnPersistEnd for Policy native contract: %w", err) 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 { if err := bc.contracts.Designate.OnPersistEnd(bc.dao); err != nil {
bc.lock.Unlock() bc.lock.Unlock()
return err return err
@ -733,7 +750,10 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
} }
bc.topBlock.Store(block) bc.topBlock.Store(block)
atomic.StoreUint32(&bc.blockHeight, block.Index) 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() bc.lock.Unlock()
updateBlockHeightMetric(block.Index) updateBlockHeightMetric(block.Index)
@ -934,6 +954,24 @@ func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint
return &neo.Balance, neo.LastUpdatedBlock 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. // LastBatch returns last persisted storage batch.
func (bc *Blockchain) LastBatch() *storage.MemBatch { func (bc *Blockchain) LastBatch() *storage.MemBatch {
return bc.lastBatch return bc.lastBatch
@ -1204,16 +1242,6 @@ func (bc *Blockchain) FeePerByte() int64 {
return bc.contracts.Policy.GetFeePerByteInternal(bc.dao) 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. // GetMemPool returns the memory pool of the blockchain.
func (bc *Blockchain) GetMemPool() *mempool.Pool { func (bc *Blockchain) GetMemPool() *mempool.Pool {
return bc.memPool return bc.memPool
@ -1279,9 +1307,10 @@ var (
// verifyAndPoolTx verifies whether a transaction is bonafide or not and tries // verifyAndPoolTx verifies whether a transaction is bonafide or not and tries
// to add it to the mempool given. // 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() 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) return fmt.Errorf("%w: ValidUntilBlock = %d, current height = %d", ErrTxExpired, t.ValidUntilBlock, height)
} }
// Policying. // Policying.
@ -1316,14 +1345,14 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return err return err
} }
} }
err := bc.verifyTxWitnesses(t, nil) err := bc.verifyTxWitnesses(t, nil, isPartialTx)
if err != nil { if err != nil {
return err return err
} }
if err := bc.verifyTxAttributes(t); err != nil { if err := bc.verifyTxAttributes(t, isPartialTx); err != nil {
return err return err
} }
err = pool.Add(t, bc) err = pool.Add(t, feer, data...)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, mempool.ErrConflict): case errors.Is(err, mempool.ErrConflict):
@ -1344,7 +1373,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return nil 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 { for i := range tx.Attributes {
switch attrType := tx.Attributes[i].Type; attrType { switch attrType := tx.Attributes[i].Type; attrType {
case transaction.HighPriority: case transaction.HighPriority:
@ -1384,9 +1413,19 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
if !bc.config.P2PSigExtensions { if !bc.config.P2PSigExtensions {
return fmt.Errorf("%w: NotValidBefore attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) return fmt.Errorf("%w: NotValidBefore attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
} }
nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore) nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore).Height
if height := bc.BlockHeight(); height < nvb.Height { if isPartialTx {
return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb.Height, height) 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: case transaction.ConflictsT:
if !bc.config.P2PSigExtensions { if !bc.config.P2PSigExtensions {
@ -1412,13 +1451,13 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
return nil 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 // new block addition. It returns false for transactions added by the new block
// (passed via txpool) and does witness reverification for non-standard // (passed via txpool) and does witness reverification for non-standard
// contracts. It operates under the assumption that full transaction verification // 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 // 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. // 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 recheckWitness bool
var curheight = bc.BlockHeight() var curheight = bc.BlockHeight()
@ -1432,7 +1471,7 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *memp
} else if txpool.HasConflicts(t, bc) { } else if txpool.HasConflicts(t, bc) {
return false return false
} }
if err := bc.verifyTxAttributes(t); err != nil { if err := bc.verifyTxAttributes(t, isPartialTx); err != nil {
return false return false
} }
for i := range t.Scripts { for i := range t.Scripts {
@ -1442,7 +1481,7 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *memp
} }
} }
if recheckWitness { if recheckWitness {
return bc.verifyTxWitnesses(t, nil) == nil return bc.verifyTxWitnesses(t, nil, isPartialTx) == nil
} }
return true return true
@ -1525,10 +1564,10 @@ func (bc *Blockchain) verifyStateRootWitness(r *state.MPTRoot) error {
// current blockchain state. Note that this verification is completely isolated // current blockchain state. Note that this verification is completely isolated
// from the main node's mempool. // from the main node's mempool.
func (bc *Blockchain) VerifyTx(t *transaction.Transaction) error { func (bc *Blockchain) VerifyTx(t *transaction.Transaction) error {
var mp = mempool.New(1) var mp = mempool.New(1, 0)
bc.lock.RLock() bc.lock.RLock()
defer bc.lock.RUnlock() 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 // 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 { if len(pools) == 1 {
pool = pools[0] 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. //GetStandByValidators returns validators from the configuration.
@ -1595,6 +1648,7 @@ var (
ErrWitnessHashMismatch = errors.New("witness hash mismatch") ErrWitnessHashMismatch = errors.New("witness hash mismatch")
ErrNativeContractWitness = errors.New("native contract witness must have empty verification script") ErrNativeContractWitness = errors.New("native contract witness must have empty verification script")
ErrVerificationFailed = errors.New("signature check failed") ErrVerificationFailed = errors.New("signature check failed")
ErrInvalidSignature = fmt.Errorf("%w: invalid signature", ErrVerificationFailed)
ErrUnknownVerificationContract = errors.New("unknown verification contract") ErrUnknownVerificationContract = errors.New("unknown verification contract")
ErrInvalidVerificationContract = errors.New("verification contract is missing `verify` method") ErrInvalidVerificationContract = errors.New("verification contract is missing `verify` method")
) )
@ -1670,12 +1724,12 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
if err != nil { if err != nil {
return 0, fmt.Errorf("%w: invalid return value", ErrVerificationFailed) return 0, fmt.Errorf("%w: invalid return value", ErrVerificationFailed)
} }
if !res {
return 0, fmt.Errorf("%w: invalid signature", ErrVerificationFailed)
}
if vm.Estack().Len() != 0 { if vm.Estack().Len() != 0 {
return 0, fmt.Errorf("%w: expected exactly one returned value", ErrVerificationFailed) return 0, fmt.Errorf("%w: expected exactly one returned value", ErrVerificationFailed)
} }
if !res {
return vm.GasConsumed(), ErrInvalidSignature
}
} else { } else {
return 0, fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed) return 0, fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed)
} }
@ -1688,15 +1742,23 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
// is used for easy interop access and can be omitted for transactions that are // is used for easy interop access and can be omitted for transactions that are
// not yet added into any block. // 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). // 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) { if len(t.Signers) != len(t.Scripts) {
return fmt.Errorf("%w: %d vs %d", ErrTxInvalidWitnessNum, len(t.Signers), len(t.Scripts)) return fmt.Errorf("%w: %d vs %d", ErrTxInvalidWitnessNum, len(t.Signers), len(t.Scripts))
} }
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t) interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t)
gasLimit := t.NetworkFee - int64(t.Size())*bc.FeePerByte() 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 { for i := range t.Signers {
gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit) 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) return fmt.Errorf("witness #%d: %w", i, err)
} }
gasLimit -= gasConsumed gasLimit -= gasConsumed
@ -1749,3 +1811,33 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block *
func (bc *Blockchain) P2PSigExtensionsEnabled() bool { func (bc *Blockchain) P2PSigExtensionsEnabled() bool {
return bc.config.P2PSigExtensions 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.

View file

@ -14,6 +14,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/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/chaindump"
"github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "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)) require.True(t, errors.Is(err, ErrAlreadyExists))
}) })
t.Run("MemPoolOOM", func(t *testing.T) { t.Run("MemPoolOOM", func(t *testing.T) {
bc.memPool = mempool.New(1) bc.memPool = mempool.New(1, 0)
tx1 := bc.newTestTx(h, testScript) tx1 := bc.newTestTx(h, testScript)
tx1.NetworkFee += 10000 // Give it more priority. tx1.NetworkFee += 10000 // Give it more priority.
require.NoError(t, accs[0].SignTx(tx1)) 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) { func TestVerifyHashAgainstScript(t *testing.T) {
@ -1021,9 +1112,9 @@ func TestIsTxStillRelevant(t *testing.T) {
tx := newTx(t) tx := newTx(t)
require.NoError(t, testchain.SignTx(bc, tx)) 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.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) { t.Run("tx is already persisted", func(t *testing.T) {
@ -1031,9 +1122,9 @@ func TestIsTxStillRelevant(t *testing.T) {
tx.ValidUntilBlock = bc.BlockHeight() + 2 tx.ValidUntilBlock = bc.BlockHeight() + 2
require.NoError(t, testchain.SignTx(bc, tx)) 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.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) { 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.NoError(t, testchain.SignTx(bc, tx2))
require.True(t, bc.isTxStillRelevant(tx1, mp)) require.True(t, bc.IsTxStillRelevant(tx1, mp, false))
require.NoError(t, bc.verifyAndPoolTx(tx2, mp)) require.NoError(t, bc.verifyAndPoolTx(tx2, mp, bc))
require.False(t, bc.isTxStillRelevant(tx1, mp)) require.False(t, bc.IsTxStillRelevant(tx1, mp, false))
}) })
t.Run("NotValidBefore", func(t *testing.T) { t.Run("NotValidBefore", func(t *testing.T) {
tx3 := newTx(t) tx3 := newTx(t)
@ -1060,9 +1151,9 @@ func TestIsTxStillRelevant(t *testing.T) {
tx3.ValidUntilBlock = bc.BlockHeight() + 2 tx3.ValidUntilBlock = bc.BlockHeight() + 2
require.NoError(t, testchain.SignTx(bc, tx3)) 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.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) { t.Run("contract witness check fails", func(t *testing.T) {
src := fmt.Sprintf(`package verify src := fmt.Sprintf(`package verify
@ -1087,9 +1178,9 @@ func TestIsTxStillRelevant(t *testing.T) {
require.NoError(t, testchain.SignTx(bc, tx)) require.NoError(t, testchain.SignTx(bc, tx))
tx.Scripts = append(tx.Scripts, transaction.Witness{}) 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.NoError(t, bc.AddBlock(bc.newBlock()))
require.False(t, bc.isTxStillRelevant(tx, mp)) require.False(t, bc.IsTxStillRelevant(tx, mp, false))
}) })
} }

View file

@ -25,6 +25,7 @@ type Blockchainer interface {
AddStateRoot(r *state.MPTRoot) error AddStateRoot(r *state.MPTRoot) error
CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error) CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error)
Close() Close()
IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool
HeaderHeight() uint32 HeaderHeight() uint32
GetBlock(hash util.Uint256) (*block.Block, error) GetBlock(hash util.Uint256) (*block.Block, error)
GetCommittee() (keys.PublicKeys, error) GetCommittee() (keys.PublicKeys, error)
@ -40,9 +41,13 @@ type Blockchainer interface {
HasBlock(util.Uint256) bool HasBlock(util.Uint256) bool
HasTransaction(util.Uint256) bool HasTransaction(util.Uint256) bool
GetAppExecResults(util.Uint256, trigger.Type) ([]state.AppExecResult, error) GetAppExecResults(util.Uint256, trigger.Type) ([]state.AppExecResult, error)
GetNotaryDepositExpiration(acc util.Uint160) uint32
GetNativeContractScriptHash(string) (util.Uint160, error) GetNativeContractScriptHash(string) (util.Uint160, error)
GetNextBlockValidators() ([]*keys.PublicKey, error) GetNextBlockValidators() ([]*keys.PublicKey, error)
GetNEP17Balances(util.Uint160) *state.NEP17Balances GetNEP17Balances(util.Uint160) *state.NEP17Balances
GetNotaryContractScriptHash() util.Uint160
GetNotaryBalance(acc util.Uint160) *big.Int
GetPolicer() Policer
GetValidators() ([]*keys.PublicKey, error) GetValidators() ([]*keys.PublicKey, error)
GetStandByCommittee() keys.PublicKeys GetStandByCommittee() keys.PublicKeys
GetStandByValidators() keys.PublicKeys GetStandByValidators() keys.PublicKeys
@ -53,9 +58,9 @@ type Blockchainer interface {
GetTestVM(tx *transaction.Transaction, b *block.Block) *vm.VM GetTestVM(tx *transaction.Transaction, b *block.Block) *vm.VM
GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error)
mempool.Feer // fee interface mempool.Feer // fee interface
GetMaxBlockSize() uint32
GetMaxBlockSystemFee() int64
PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) error 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) SubscribeForBlocks(ch chan<- *block.Block)
SubscribeForExecutions(ch chan<- *state.AppExecResult) SubscribeForExecutions(ch chan<- *state.AppExecResult)
SubscribeForNotifications(ch chan<- *state.NotificationEvent) SubscribeForNotifications(ch chan<- *state.NotificationEvent)

View file

@ -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
}

View file

@ -439,6 +439,38 @@ func invokeContractMethod(chain *Blockchain, sysfee int64, hash util.Uint160, me
return &res[0], nil 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 { 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 := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...)
transferTx.SystemFee = 100000000 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) { 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, 1, len(result.Stack))
require.Equal(t, expected, result.Stack[0]) require.Equal(t, expected, result.Stack[0])
} }

View file

@ -39,6 +39,7 @@ var (
type item struct { type item struct {
txn *transaction.Transaction txn *transaction.Transaction
blockStamp uint32 blockStamp uint32
data interface{}
} }
// items is a slice of item. // items is a slice of item.
@ -64,9 +65,10 @@ type Pool struct {
capacity int capacity int
feePerByte int64 feePerByte int64
payerIndex int
resendThreshold uint32 resendThreshold uint32
resendFunc func(*transaction.Transaction) resendFunc func(*transaction.Transaction, interface{})
} }
func (p items) Len() int { return len(p) } 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 // 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 // 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 { 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 { if !ok {
senderFee.balance = feer.GetUtilityTokenBalance(tx.Sender()) senderFee.balance = feer.GetUtilityTokenBalance(payer)
senderFee.feeSum = big.NewInt(0) senderFee.feeSum = big.NewInt(0)
mp.fees[tx.Sender()] = senderFee mp.fees[payer] = senderFee
} }
if needCheck { if needCheck {
newFeeSum, err := checkBalance(tx, senderFee) 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. // 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{ var pItem = item{
txn: t, txn: t,
blockStamp: fee.BlockHeight(), blockStamp: fee.BlockHeight(),
} }
if data != nil {
pItem.data = data[0]
}
mp.lock.Lock() mp.lock.Lock()
if mp.containsKey(t.Hash()) { if mp.containsKey(t.Hash()) {
mp.lock.Unlock() mp.lock.Unlock()
@ -281,14 +287,16 @@ func (mp *Pool) removeInternal(hash util.Uint256, feer Feer) {
break break
} }
} }
itm := mp.verifiedTxes[num]
if num < len(mp.verifiedTxes)-1 { if num < len(mp.verifiedTxes)-1 {
mp.verifiedTxes = append(mp.verifiedTxes[:num], mp.verifiedTxes[num+1:]...) mp.verifiedTxes = append(mp.verifiedTxes[:num], mp.verifiedTxes[num+1:]...)
} else if num == len(mp.verifiedTxes)-1 { } else if num == len(mp.verifiedTxes)-1 {
mp.verifiedTxes = mp.verifiedTxes[:num] 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)) senderFee.feeSum.Sub(senderFee.feeSum, big.NewInt(tx.SystemFee+tx.NetworkFee))
mp.fees[tx.Sender()] = senderFee mp.fees[payer] = senderFee
if feer.P2PSigExtensionsEnabled() { if feer.P2PSigExtensionsEnabled() {
// remove all conflicting hashes from mp.conflicts list // remove all conflicting hashes from mp.conflicts list
mp.removeConflictsOf(tx) 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) mp.conflicts = make(map[util.Uint256][]util.Uint256)
} }
height := feer.BlockHeight() height := feer.BlockHeight()
var staleTxs []*transaction.Transaction var staleItems []item
for _, itm := range mp.verifiedTxes { for _, itm := range mp.verifiedTxes {
if isOK(itm.txn) && mp.checkPolicy(itm.txn, policyChanged) && mp.tryAddSendersFee(itm.txn, feer, true) { if isOK(itm.txn) && mp.checkPolicy(itm.txn, policyChanged) && mp.tryAddSendersFee(itm.txn, feer, true) {
newVerifiedTxes = append(newVerifiedTxes, itm) newVerifiedTxes = append(newVerifiedTxes, itm)
@ -325,11 +333,11 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer)
} }
} }
if mp.resendThreshold != 0 { 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. // so quotient must be a power of two.
diff := (height - itm.blockStamp) diff := (height - itm.blockStamp)
if diff%mp.resendThreshold == 0 && bits.OnesCount32(diff/mp.resendThreshold) == 1 { if diff%mp.resendThreshold == 0 && bits.OnesCount32(diff/mp.resendThreshold) == 1 {
staleTxs = append(staleTxs, itm.txn) staleItems = append(staleItems, itm)
} }
} }
} else { } else {
@ -339,8 +347,8 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer)
} }
} }
} }
if len(staleTxs) != 0 { if len(staleItems) != 0 {
go mp.resendStaleTxs(staleTxs) go mp.resendStaleItems(staleItems)
} }
mp.verifiedTxes = newVerifiedTxes mp.verifiedTxes = newVerifiedTxes
mp.lock.Unlock() mp.lock.Unlock()
@ -366,11 +374,12 @@ func (mp *Pool) checkPolicy(tx *transaction.Transaction, policyChanged bool) boo
} }
// New returns a new Pool struct. // New returns a new Pool struct.
func New(capacity int) *Pool { func New(capacity int, payerIndex int) *Pool {
return &Pool{ return &Pool{
verifiedMap: make(map[util.Uint256]*transaction.Transaction), verifiedMap: make(map[util.Uint256]*transaction.Transaction),
verifiedTxes: make([]item, 0, capacity), verifiedTxes: make([]item, 0, capacity),
capacity: capacity, capacity: capacity,
payerIndex: payerIndex,
fees: make(map[util.Uint160]utilityBalanceAndFees), fees: make(map[util.Uint160]utilityBalanceAndFees),
conflicts: make(map[util.Uint256][]util.Uint256), conflicts: make(map[util.Uint256][]util.Uint256),
oracleResp: make(map[uint64]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 // SetResendThreshold sets threshold after which transaction will be considered stale
// and returned for retransmission by `GetStaleTransactions`. // 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() mp.lock.Lock()
defer mp.lock.Unlock() defer mp.lock.Unlock()
mp.resendThreshold = h mp.resendThreshold = h
mp.resendFunc = f mp.resendFunc = f
} }
func (mp *Pool) resendStaleTxs(txs []*transaction.Transaction) { func (mp *Pool) resendStaleItems(items []item) {
for i := range txs { for i := range items {
mp.resendFunc(txs[i]) 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 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. // GetVerifiedTransactions returns a slice of transactions with their fees.
func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction { func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction {
mp.lock.RLock() mp.lock.RLock()
@ -420,9 +453,10 @@ func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction {
// checkTxConflicts is an internal unprotected version of Verify. It takes into // checkTxConflicts is an internal unprotected version of Verify. It takes into
// consideration conflicting transactions which are about to be removed from mempool. // consideration conflicting transactions which are about to be removed from mempool.
func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*transaction.Transaction, error) { 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 { if !ok {
actualSenderFee.balance = fee.GetUtilityTokenBalance(tx.Sender()) actualSenderFee.balance = fee.GetUtilityTokenBalance(payer)
actualSenderFee.feeSum = big.NewInt(0) 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 { if conflictingHashes, ok := mp.conflicts[tx.Hash()]; ok {
for _, hash := range conflictingHashes { for _, hash := range conflictingHashes {
existingTx := mp.verifiedMap[hash] 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()) return nil, fmt.Errorf("%w: conflicting transaction %s has bigger network fee", ErrConflictsAttribute, existingTx.Hash().StringBE())
} }
conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx) conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx)
@ -447,7 +481,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
if !ok { if !ok {
continue 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()) return nil, fmt.Errorf("%w: not signed by the sender of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE())
} }
if existingTx.NetworkFee >= tx.NetworkFee { 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), feeSum: new(big.Int).Set(actualSenderFee.feeSum),
} }
for _, conflictingTx := range conflictsToBeRemoved { 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)) expectedSenderFee.feeSum.Sub(expectedSenderFee.feeSum, big.NewInt(conflictingTx.SystemFee+conflictingTx.NetworkFee))
} }
} }

View file

@ -10,6 +10,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/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/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -20,10 +21,9 @@ type FeerStub struct {
feePerByte int64 feePerByte int64
p2pSigExt bool p2pSigExt bool
blockHeight uint32 blockHeight uint32
balance int64
} }
var balance = big.NewInt(10000000)
func (fs *FeerStub) FeePerByte() int64 { func (fs *FeerStub) FeePerByte() int64 {
return fs.feePerByte return fs.feePerByte
} }
@ -33,7 +33,7 @@ func (fs *FeerStub) BlockHeight() uint32 {
} }
func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int { func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
return balance return big.NewInt(fs.balance)
} }
func (fs *FeerStub) P2PSigExtensionsEnabled() bool { func (fs *FeerStub) P2PSigExtensionsEnabled() bool {
@ -41,7 +41,7 @@ func (fs *FeerStub) P2PSigExtensionsEnabled() bool {
} }
func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) { 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 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx.Nonce = 0 tx.Nonce = 0
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}} 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) { func TestMemPoolRemoveStale(t *testing.T) {
mp := New(5) mp := New(5, 0)
txs := make([]*transaction.Transaction, 5) txs := make([]*transaction.Transaction, 5)
for i := range txs { for i := range txs {
txs[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) 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) staleTxs := make(chan *transaction.Transaction, 5)
f := func(tx *transaction.Transaction) { f := func(tx *transaction.Transaction, _ interface{}) {
staleTxs <- tx staleTxs <- tx
} }
mp.SetResendThreshold(5, f) mp.SetResendThreshold(5, f)
@ -111,9 +111,9 @@ func TestMemPoolAddRemove(t *testing.T) {
} }
func TestOverCapacity(t *testing.T) { func TestOverCapacity(t *testing.T) {
var fs = &FeerStub{} var fs = &FeerStub{balance: 10000000}
const mempoolSize = 10 const mempoolSize = 10
mp := New(mempoolSize) mp := New(mempoolSize, 0)
for i := 0; i < mempoolSize; i++ { for i := 0; i < mempoolSize; i++ {
tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
@ -186,7 +186,7 @@ func TestOverCapacity(t *testing.T) {
func TestGetVerified(t *testing.T) { func TestGetVerified(t *testing.T) {
var fs = &FeerStub{} var fs = &FeerStub{}
const mempoolSize = 10 const mempoolSize = 10
mp := New(mempoolSize) mp := New(mempoolSize, 0)
txes := make([]*transaction.Transaction, 0, mempoolSize) txes := make([]*transaction.Transaction, 0, mempoolSize)
for i := 0; i < mempoolSize; i++ { for i := 0; i < mempoolSize; i++ {
@ -210,7 +210,7 @@ func TestGetVerified(t *testing.T) {
func TestRemoveStale(t *testing.T) { func TestRemoveStale(t *testing.T) {
var fs = &FeerStub{} var fs = &FeerStub{}
const mempoolSize = 10 const mempoolSize = 10
mp := New(mempoolSize) mp := New(mempoolSize, 0)
txes1 := make([]*transaction.Transaction, 0, mempoolSize/2) txes1 := make([]*transaction.Transaction, 0, mempoolSize/2)
txes2 := 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) { func TestMemPoolFees(t *testing.T) {
mp := New(10) mp := New(10, 0)
fs := &FeerStub{balance: 10000000}
sender0 := util.Uint160{1, 2, 3} sender0 := util.Uint160{1, 2, 3}
tx0 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) 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}} tx0.Signers = []transaction.Signer{{Account: sender0}}
// insufficient funds to add transaction, and balance shouldn't be stored // insufficient funds to add transaction, and balance shouldn't be stored
require.Equal(t, false, mp.Verify(tx0, &FeerStub{})) require.Equal(t, false, mp.Verify(tx0, fs))
require.Error(t, mp.Add(tx0, &FeerStub{})) require.Error(t, mp.Add(tx0, fs))
require.Equal(t, 0, len(mp.fees)) 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 // no problems with adding another transaction with lower fee
tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx1.NetworkFee = balancePart.Int64() tx1.NetworkFee = balancePart.Int64()
tx1.Signers = []transaction.Signer{{Account: sender0}} 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, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{ require.Equal(t, utilityBalanceAndFees{
balance: balance, balance: big.NewInt(fs.balance),
feeSum: big.NewInt(tx1.NetworkFee), feeSum: big.NewInt(tx1.NetworkFee),
}, mp.fees[sender0]) }, mp.fees[sender0])
// balance shouldn't change after adding one more transaction // balance shouldn't change after adding one more transaction
tx2 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) 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}} 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, 2, len(mp.verifiedTxes))
require.Equal(t, 1, len(mp.fees)) require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{ require.Equal(t, utilityBalanceAndFees{
balance: balance, balance: big.NewInt(fs.balance),
feeSum: balance, feeSum: big.NewInt(fs.balance),
}, mp.fees[sender0]) }, mp.fees[sender0])
// can't add more transactions as we don't have enough GAS // can't add more transactions as we don't have enough GAS
tx3 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx3 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx3.NetworkFee = 1 tx3.NetworkFee = 1
tx3.Signers = []transaction.Signer{{Account: sender0}} tx3.Signers = []transaction.Signer{{Account: sender0}}
require.Equal(t, false, mp.Verify(tx3, &FeerStub{})) require.Equal(t, false, mp.Verify(tx3, fs))
require.Error(t, mp.Add(tx3, &FeerStub{})) require.Error(t, mp.Add(tx3, fs))
require.Equal(t, 1, len(mp.fees)) require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{ require.Equal(t, utilityBalanceAndFees{
balance: balance, balance: big.NewInt(fs.balance),
feeSum: balance, feeSum: big.NewInt(fs.balance),
}, mp.fees[sender0]) }, mp.fees[sender0])
// check whether sender's fee updates correctly // check whether sender's fee updates correctly
@ -295,10 +296,10 @@ func TestMemPoolFees(t *testing.T) {
return true return true
} }
return false return false
}, &FeerStub{}) }, fs)
require.Equal(t, 1, len(mp.fees)) require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{ require.Equal(t, utilityBalanceAndFees{
balance: balance, balance: big.NewInt(fs.balance),
feeSum: big.NewInt(tx2.NetworkFee), feeSum: big.NewInt(tx2.NetworkFee),
}, mp.fees[sender0]) }, mp.fees[sender0])
@ -308,12 +309,13 @@ func TestMemPoolFees(t *testing.T) {
return true return true
} }
return false return false
}, &FeerStub{}) }, fs)
require.Equal(t, 0, len(mp.fees)) require.Equal(t, 0, len(mp.fees))
} }
func TestMempoolItemsOrder(t *testing.T) { func TestMempoolItemsOrder(t *testing.T) {
sender0 := util.Uint160{1, 2, 3} sender0 := util.Uint160{1, 2, 3}
balance := big.NewInt(10000000)
tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx1.NetworkFee = new(big.Int).Div(balance, big.NewInt(8)).Int64() 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) { func TestMempoolAddRemoveOracleResponse(t *testing.T) {
mp := New(5) mp := New(5, 0)
nonce := uint32(0) nonce := uint32(0)
fs := &FeerStub{} fs := &FeerStub{balance: 10000}
newTx := func(netFee int64, id uint64) *transaction.Transaction { newTx := func(netFee int64, id uint64) *transaction.Transaction {
tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0) tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx.NetworkFee = netFee tx.NetworkFee = netFee
@ -406,9 +408,9 @@ func TestMempoolAddRemoveOracleResponse(t *testing.T) {
func TestMempoolAddRemoveConflicts(t *testing.T) { func TestMempoolAddRemoveConflicts(t *testing.T) {
capacity := 6 capacity := 6
mp := New(capacity) mp := New(capacity, 0)
var ( var (
fs = &FeerStub{p2pSigExt: true} fs = &FeerStub{p2pSigExt: true, balance: 100000}
nonce uint32 = 1 nonce uint32 = 1
) )
getConflictsTx := func(netFee int64, hashes ...util.Uint256) *transaction.Transaction { getConflictsTx := func(netFee int64, hashes ...util.Uint256) *transaction.Transaction {
@ -524,3 +526,116 @@ func TestMempoolAddRemoveConflicts(t *testing.T) {
require.Equal(t, false, ok) require.Equal(t, false, ok)
require.True(t, errors.Is(mp.Add(tx13, fs), ErrConflictsAttribute)) 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)
}

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "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"
@ -26,17 +27,29 @@ type Notary struct {
interop.ContractMD interop.ContractMD
GAS *GAS GAS *GAS
Desig *Designate 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 ( const (
notaryName = "Notary" notaryName = "Notary"
notaryContractID = reservedContractID - 1 notaryContractID = reservedContractID - 1
// NotaryVerificationPrice is the price of `verify` Notary method.
NotaryVerificationPrice = 100_0000
// prefixDeposit is a prefix for storing Notary deposits. // prefixDeposit is a prefix for storing Notary deposits.
prefixDeposit = 1 prefixDeposit = 1
defaultDepositDeltaTill = 5760 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. // newNotary returns Notary native contract.
func newNotary() *Notary { func newNotary() *Notary {
n := &Notary{ContractMD: *interop.NewContractMD(notaryName)} n := &Notary{ContractMD: *interop.NewContractMD(notaryName)}
@ -76,6 +89,15 @@ func newNotary() *Notary {
md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates) md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates)
n.AddMethod(md, desc) 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) desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates)
n.AddMethod(md, desc) n.AddMethod(md, desc)
@ -94,6 +116,8 @@ func (n *Notary) Metadata() *interop.ContractMD {
// Initialize initializes Notary native contract and implements Contract interface. // Initialize initializes Notary native contract and implements Contract interface.
func (n *Notary) Initialize(ic *interop.Context) error { func (n *Notary) Initialize(ic *interop.Context) error {
n.isValid = true
n.maxNotValidBeforeDelta = defaultMaxNotValidBeforeDelta
return nil return nil
} }
@ -116,7 +140,7 @@ func (n *Notary) OnPersist(ic *interop.Context) error {
nFees += int64(nKeys) + 1 nFees += int64(nKeys) + 1
if tx.Sender() == n.Hash { if tx.Sender() == n.Hash {
payer := tx.Signers[1] 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)) balance.Amount.Sub(balance.Amount, big.NewInt(tx.SystemFee+tx.NetworkFee))
if balance.Amount.Sign() == 0 { if balance.Amount.Sign() == 0 {
err := n.removeDepositFor(ic.DAO, payer.Account) err := n.removeDepositFor(ic.DAO, payer.Account)
@ -142,6 +166,19 @@ func (n *Notary) OnPersist(ic *interop.Context) error {
return nil 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 // onPayment records deposited amount as belonging to "from" address with a lock
// till the specified chain's height. // till the specified chain's height.
func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item { 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 allowedChangeTill := ic.Tx.Sender() == to
currentHeight := ic.Chain.BlockHeight() currentHeight := ic.Chain.BlockHeight()
deposit := n.getDepositFor(ic.DAO, to) deposit := n.GetDepositFor(ic.DAO, to)
till := toUint32(additionalParams[1]) till := toUint32(additionalParams[1])
if till < currentHeight { if till < currentHeight {
panic(fmt.Errorf("`till` shouldn't be less then the chain's height %d", 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() { if till < ic.Chain.BlockHeight() {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
deposit := n.getDepositFor(ic.DAO, addr) deposit := n.GetDepositFor(ic.DAO, addr)
if deposit == nil { if deposit == nil {
return stackitem.NewBool(false) 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{}) { if !args[1].Equals(stackitem.Null{}) {
to = toUint160(args[1]) to = toUint160(args[1])
} }
deposit := n.getDepositFor(ic.DAO, from) deposit := n.GetDepositFor(ic.DAO, from)
if deposit == nil { if deposit == nil {
return stackitem.NewBool(false) 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. // balanceOf returns deposited GAS amount for specified address.
func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
acc := toUint160(args[0]) 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 { 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. // expirationOf Returns deposit lock height for specified address.
func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
acc := toUint160(args[0]) 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 { 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. // 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) return stackitem.NewBool(false)
} }
payer := tx.Signers[1].Account 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 { if balance == nil || balance.Amount.Cmp(big.NewInt(tx.NetworkFee+tx.SystemFee)) < 0 {
return stackitem.NewBool(false) return stackitem.NewBool(false)
} }
@ -328,9 +375,47 @@ func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) {
return nodes, err 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. // 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()...) key := append([]byte{prefixDeposit}, acc.BytesBE()...)
deposit := new(state.Deposit) deposit := new(state.Deposit)
err := getSerializableFromDAO(n.ContractID, dao, key, deposit) err := getSerializableFromDAO(n.ContractID, dao, key, deposit)

View file

@ -1,7 +1,6 @@
package native package native
import ( import (
"encoding/binary"
"fmt" "fmt"
"math/big" "math/big"
"sort" "sort"
@ -10,7 +9,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/block" "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/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop" "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/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/network/payload"
@ -167,10 +165,10 @@ func (p *Policy) OnPersistEnd(dao dao.DAO) error {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
p.maxTransactionsPerBlock = p.getUint32WithKey(dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock) p.maxTransactionsPerBlock = getUint32WithKey(p.ContractID, dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock)
p.maxBlockSize = p.getUint32WithKey(dao, maxBlockSizeKey, defaultMaxBlockSize) p.maxBlockSize = getUint32WithKey(p.ContractID, dao, maxBlockSizeKey, defaultMaxBlockSize)
p.feePerByte = p.getInt64WithKey(dao, feePerByteKey, defaultFeePerByte) p.feePerByte = getInt64WithKey(p.ContractID, dao, feePerByteKey, defaultFeePerByte)
p.maxBlockSystemFee = p.getInt64WithKey(dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) p.maxBlockSystemFee = getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee)
p.maxVerificationGas = defaultMaxVerificationGas p.maxVerificationGas = defaultMaxVerificationGas
p.blockedAccounts = make([]util.Uint160, 0) p.blockedAccounts = make([]util.Uint160, 0)
@ -207,7 +205,7 @@ func (p *Policy) GetMaxTransactionsPerBlockInternal(dao dao.DAO) uint32 {
if p.isValid { if p.isValid {
return p.maxTransactionsPerBlock 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. // 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 { if p.isValid {
return p.maxBlockSize 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 // 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 { if p.isValid {
return p.feePerByte 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. // 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 { if p.isValid {
return p.maxBlockSystemFee 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. // 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 { if value > block.MaxTransactionsPerBlock {
panic(fmt.Errorf("MaxTransactionsPerBlock cannot exceed the maximum allowed transactions per block = %d", 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 { if err != nil {
panic(err) panic(err)
} }
@ -305,7 +303,7 @@ func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackite
} }
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
err = p.setUint32WithKey(ic.DAO, maxTransactionsPerBlockKey, value) err = setUint32WithKey(p.ContractID, ic.DAO, maxTransactionsPerBlockKey, value)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -319,7 +317,7 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta
if value > payload.MaxSize { if value > payload.MaxSize {
panic(fmt.Errorf("MaxBlockSize cannot be more than the maximum payload size = %d", 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 { if err != nil {
panic(err) panic(err)
} }
@ -328,7 +326,7 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta
} }
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
err = p.setUint32WithKey(ic.DAO, maxBlockSizeKey, value) err = setUint32WithKey(p.ContractID, ic.DAO, maxBlockSizeKey, value)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -342,7 +340,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack
if value < 0 || value > maxFeePerByte { if value < 0 || value > maxFeePerByte {
panic(fmt.Errorf("FeePerByte shouldn't be negative or greater than %d", 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 { if err != nil {
panic(err) panic(err)
} }
@ -351,7 +349,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack
} }
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
err = p.setInt64WithKey(ic.DAO, feePerByteKey, value) err = setInt64WithKey(p.ContractID, ic.DAO, feePerByteKey, value)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -365,7 +363,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item
if value <= minBlockSystemFee { if value <= minBlockSystemFee {
panic(fmt.Errorf("MaxBlockSystemFee cannot be less then %d", minBlockSystemFee)) panic(fmt.Errorf("MaxBlockSystemFee cannot be less then %d", minBlockSystemFee))
} }
ok, err := p.checkValidators(ic) ok, err := checkValidators(ic)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -374,7 +372,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item
} }
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
err = p.setInt64WithKey(ic.DAO, maxBlockSystemFeeKey, value) err = setInt64WithKey(p.ContractID, ic.DAO, maxBlockSystemFeeKey, value)
if err != nil { if err != nil {
panic(err) 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 // blockAccount is Policy contract method and adds given account hash to the list
// of blocked accounts. // of blocked accounts.
func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
ok, err := p.checkValidators(ic) ok, err := checkValidators(ic)
if err != nil { if err != nil {
panic(err) 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 // unblockAccount is Policy contract method and removes given account hash from
// the list of blocked accounts. // the list of blocked accounts.
func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
ok, err := p.checkValidators(ic) ok, err := checkValidators(ic)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -434,46 +432,6 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
return stackitem.NewBool(true) 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 // CheckPolicy checks whether transaction conforms to current policy restrictions
// like not being signed by blocked account or not exceeding block-level system // like not being signed by blocked account or not exceeding block-level system
// fee limit. // fee limit.

View file

@ -1,7 +1,11 @@
package native package native
import ( import (
"encoding/binary"
"github.com/nspcc-dev/neo-go/pkg/core/dao" "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/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/io" "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(), 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)
}

View file

@ -2,12 +2,14 @@ package core
import ( import (
"math" "math"
"math/big"
"testing" "testing"
"github.com/nspcc-dev/neo-go/internal/testchain" "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/native"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/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/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
@ -319,3 +321,48 @@ func TestNotaryNodesReward(t *testing.T) {
checkReward(5, 7, spendDeposit) 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))
})
}

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -45,6 +46,13 @@ func TestMaxTransactionsPerBlock(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
checkFAULTState(t, res) 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) { func TestMaxBlockSize(t *testing.T) {
@ -80,6 +88,13 @@ func TestMaxBlockSize(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
checkFAULTState(t, res) 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) { func TestFeePerByte(t *testing.T) {
@ -119,6 +134,13 @@ func TestFeePerByte(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
checkFAULTState(t, res) 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) { func TestBlockSystemFee(t *testing.T) {
@ -154,6 +176,13 @@ func TestBlockSystemFee(t *testing.T) {
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000))) checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000)))
require.NoError(t, chain.persist()) 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) { func TestBlockedAccounts(t *testing.T) {
@ -217,4 +246,11 @@ func TestBlockedAccounts(t *testing.T) {
checkResult(t, res, stackitem.NewBool(false)) checkResult(t, res, stackitem.NewBool(false))
require.NoError(t, chain.persist()) 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))
})
} }

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/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/mempool"
"github.com/nspcc-dev/neo-go/pkg/core/state" "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/core/transaction"
@ -31,21 +32,30 @@ import (
type testChain struct { type testChain struct {
config.ProtocolConfiguration config.ProtocolConfiguration
*mempool.Pool *mempool.Pool
blocksCh []chan<- *block.Block blocksCh []chan<- *block.Block
blockheight uint32 blockheight uint32
poolTx func(*transaction.Transaction) error poolTx func(*transaction.Transaction) error
blocks map[util.Uint256]*block.Block poolTxWithData func(*transaction.Transaction, interface{}, *mempool.Pool) error
hdrHashes map[uint32]util.Uint256 blocks map[util.Uint256]*block.Block
txs map[util.Uint256]*transaction.Transaction 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 { func newTestChain() *testChain {
return &testChain{ return &testChain{
Pool: mempool.New(10), Pool: mempool.New(10, 0),
poolTx: func(*transaction.Transaction) error { return nil }, poolTx: func(*transaction.Transaction) error { return nil },
blocks: make(map[util.Uint256]*block.Block), poolTxWithData: func(*transaction.Transaction, interface{}, *mempool.Pool) error { return nil },
hdrHashes: make(map[uint32]util.Uint256), blocks: make(map[util.Uint256]*block.Block),
txs: make(map[util.Uint256]*transaction.Transaction), 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 { func (chain *testChain) ApplyPolicyToTxSet([]*transaction.Transaction) []*transaction.Transaction {
panic("TODO") 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 { func (chain *testChain) GetConfig() config.ProtocolConfiguration {
return chain.ProtocolConfiguration return chain.ProtocolConfiguration
} }
@ -77,7 +129,7 @@ func (chain *testChain) FeePerByte() int64 {
} }
func (chain *testChain) P2PSigExtensionsEnabled() bool { func (chain *testChain) P2PSigExtensionsEnabled() bool {
return false return true
} }
func (chain *testChain) GetMaxBlockSystemFee() int64 { 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 { func (chain *testChain) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
if chain.utilityTokenBalance != nil {
return chain.utilityTokenBalance
}
panic("TODO") panic("TODO")
} }
@ -230,7 +285,10 @@ func (chain *testChain) SubscribeForTransactions(ch chan<- *transaction.Transact
func (chain *testChain) VerifyTx(*transaction.Transaction) error { func (chain *testChain) VerifyTx(*transaction.Transaction) error {
panic("TODO") 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") panic("TODO")
} }

View file

@ -64,18 +64,19 @@ const (
CMDPong CommandType = 0x19 CMDPong CommandType = 0x19
// synchronization // synchronization
CMDGetHeaders CommandType = 0x20 CMDGetHeaders CommandType = 0x20
CMDHeaders CommandType = 0x21 CMDHeaders CommandType = 0x21
CMDGetBlocks CommandType = 0x24 CMDGetBlocks CommandType = 0x24
CMDMempool CommandType = 0x25 CMDMempool CommandType = 0x25
CMDInv CommandType = 0x27 CMDInv CommandType = 0x27
CMDGetData CommandType = 0x28 CMDGetData CommandType = 0x28
CMDGetBlockByIndex CommandType = 0x29 CMDGetBlockByIndex CommandType = 0x29
CMDNotFound CommandType = 0x2a CMDNotFound CommandType = 0x2a
CMDTX = CommandType(payload.TXType) CMDTX = CommandType(payload.TXType)
CMDBlock = CommandType(payload.BlockType) CMDBlock = CommandType(payload.BlockType)
CMDConsensus = CommandType(payload.ConsensusType) CMDConsensus = CommandType(payload.ConsensusType)
CMDReject CommandType = 0x2f CMDP2PNotaryRequest = CommandType(payload.P2PNotaryRequestType)
CMDReject CommandType = 0x2f
// SPV protocol // SPV protocol
CMDFilterLoad CommandType = 0x30 CMDFilterLoad CommandType = 0x30
@ -148,6 +149,8 @@ func (m *Message) decodePayload() error {
p = block.New(m.Network, m.StateRootInHeader) p = block.New(m.Network, m.StateRootInHeader)
case CMDConsensus: case CMDConsensus:
p = consensus.NewPayload(m.Network, m.StateRootInHeader) p = consensus.NewPayload(m.Network, m.StateRootInHeader)
case CMDP2PNotaryRequest:
p = &payload.P2PNotaryRequest{Network: m.Network}
case CMDGetBlocks: case CMDGetBlocks:
p = &payload.GetBlocks{} p = &payload.GetBlocks{}
case CMDGetHeaders: case CMDGetHeaders:

View file

@ -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,
}
}

View file

@ -20,21 +20,24 @@ func (i InventoryType) String() string {
return "block" return "block"
case ConsensusType: case ConsensusType:
return "consensus" return "consensus"
case P2PNotaryRequestType:
return "p2pNotaryRequest"
default: default:
return "unknown inventory type" return "unknown inventory type"
} }
} }
// Valid returns true if the inventory (type) is known. // Valid returns true if the inventory (type) is known.
func (i InventoryType) Valid() bool { func (i InventoryType) Valid(p2pSigExtensionsEnabled bool) bool {
return i == BlockType || i == TXType || i == ConsensusType return i == BlockType || i == TXType || i == ConsensusType || (p2pSigExtensionsEnabled && i == P2PNotaryRequestType)
} }
// List of valid InventoryTypes. // List of valid InventoryTypes.
const ( const (
TXType InventoryType = 0x2b TXType InventoryType = 0x2b
BlockType InventoryType = 0x2c BlockType InventoryType = 0x2c
ConsensusType InventoryType = 0x2d ConsensusType InventoryType = 0x2d
P2PNotaryRequestType InventoryType = 0x50
) )
// Inventory payload. // Inventory payload.

View file

@ -31,15 +31,22 @@ func TestEmptyInv(t *testing.T) {
} }
func TestValid(t *testing.T) { func TestValid(t *testing.T) {
require.True(t, TXType.Valid()) require.True(t, TXType.Valid(false))
require.True(t, BlockType.Valid()) require.True(t, TXType.Valid(true))
require.True(t, ConsensusType.Valid()) require.True(t, BlockType.Valid(false))
require.False(t, InventoryType(0xFF).Valid()) 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) { func TestString(t *testing.T) {
require.Equal(t, "TX", TXType.String()) require.Equal(t, "TX", TXType.String())
require.Equal(t, "block", BlockType.String()) require.Equal(t, "block", BlockType.String())
require.Equal(t, "consensus", ConsensusType.String()) require.Equal(t, "consensus", ConsensusType.String())
require.Equal(t, "p2pNotaryRequest", P2PNotaryRequestType.String())
require.True(t, strings.Contains(InventoryType(0xFF).String(), "unknown")) require.True(t, strings.Contains(InventoryType(0xFF).String(), "unknown"))
} }

View file

@ -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
}

View file

@ -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))
}

View file

@ -16,6 +16,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core" "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/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "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/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/network/capability" "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/network/payload"
@ -59,11 +60,13 @@ type (
// stateRootInHeader specifies if block header contain state root. // stateRootInHeader specifies if block header contain state root.
stateRootInHeader bool stateRootInHeader bool
transport Transporter transport Transporter
discovery Discoverer discovery Discoverer
chain blockchainer.Blockchainer chain blockchainer.Blockchainer
bQueue *blockQueue bQueue *blockQueue
consensus consensus.Service consensus consensus.Service
notaryRequestPool *mempool.Pool
NotaryFeer NotaryFeer
lock sync.RWMutex lock sync.RWMutex
peers map[Peer]bool peers map[Peer]bool
@ -124,6 +127,15 @@ func newServerFromConstructors(config ServerConfig, chain blockchainer.Blockchai
log: log, log: log,
transactions: make(chan *transaction.Transaction, 64), 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) { s.bQueue = newBlockQueue(maxBlockBatch, chain, log, func(b *block.Block) {
if !s.consensusStarted.Load() { if !s.consensusStarted.Load() {
s.tryStartConsensus() s.tryStartConsensus()
@ -188,7 +200,7 @@ func (s *Server) Start(errChan chan error) {
zap.Uint32("headerHeight", s.chain.HeaderHeight())) zap.Uint32("headerHeight", s.chain.HeaderHeight()))
s.tryStartConsensus() s.tryStartConsensus()
s.initStaleTxMemPool() s.initStaleMemPools()
go s.broadcastTxLoop() go s.broadcastTxLoop()
go s.relayBlocksLoop() go s.relayBlocksLoop()
@ -507,6 +519,9 @@ func (s *Server) handleInvCmd(p Peer, inv *payload.Inventory) error {
cp := s.consensus.GetPayload(h) cp := s.consensus.GetPayload(h)
return cp != nil return cp != nil
}, },
payload.P2PNotaryRequestType: func(h util.Uint256) bool {
return s.notaryRequestPool.ContainsKey(h)
},
} }
if exists := typExists[inv.Type]; exists != nil { if exists := typExists[inv.Type]; exists != nil {
for _, hash := range inv.Hashes { 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 { if cp := s.consensus.GetPayload(hash); cp != nil {
msg = NewMessage(CMDConsensus, cp) 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 { if msg != nil {
pkt, err := msg.Bytes() pkt, err := msg.Bytes()
@ -687,11 +708,62 @@ func (s *Server) handleTxCmd(tx *transaction.Transaction) error {
// in the pool. // in the pool.
if s.verifyAndPoolTX(tx) == RelaySucceed { if s.verifyAndPoolTX(tx) == RelaySucceed {
s.consensus.OnTransaction(tx) s.consensus.OnTransaction(tx)
s.broadcastTX(tx) s.broadcastTX(tx, nil)
} }
return 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. // handleAddrCmd will process received addresses.
func (s *Server) handleAddrCmd(p Peer, addrs *payload.AddressList) error { func (s *Server) handleAddrCmd(p Peer, addrs *payload.AddressList) error {
if !p.CanProcessAddr() { if !p.CanProcessAddr() {
@ -770,7 +842,7 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error {
if peer.Handshaked() { if peer.Handshaked() {
if inv, ok := msg.Payload.(*payload.Inventory); ok { 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 return errInvalidInvType
} }
} }
@ -808,6 +880,9 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error {
case CMDTX: case CMDTX:
tx := msg.Payload.(*transaction.Transaction) tx := msg.Payload.(*transaction.Transaction)
return s.handleTxCmd(tx) return s.handleTxCmd(tx)
case CMDP2PNotaryRequest:
r := msg.Payload.(*payload.P2PNotaryRequest)
return s.handleP2PNotaryRequestCmd(r)
case CMDPing: case CMDPing:
ping := msg.Payload.(*payload.Ping) ping := msg.Payload.(*payload.Ping)
return s.handlePing(peer, 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 { func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason {
ret := s.verifyAndPoolTX(t) ret := s.verifyAndPoolTX(t)
if ret == RelaySucceed { if ret == RelaySucceed {
s.broadcastTX(t) s.broadcastTX(t, nil)
} }
return ret return ret
} }
// broadcastTX broadcasts an inventory message about new transaction. // broadcastTX broadcasts an inventory message about new transaction.
func (s *Server) broadcastTX(t *transaction.Transaction) { func (s *Server) broadcastTX(t *transaction.Transaction, _ interface{}) {
select { select {
case s.transactions <- t: case s.transactions <- t:
case <-s.quit: case <-s.quit:
@ -964,8 +1039,8 @@ func (s *Server) broadcastTxHashes(hs []util.Uint256) {
s.iteratePeersWithSendMsg(msg, Peer.EnqueuePacket, Peer.IsFullNode) s.iteratePeersWithSendMsg(msg, Peer.EnqueuePacket, Peer.IsFullNode)
} }
// initStaleTxMemPool initializes mempool for stale tx processing. // initStaleMemPools initializes mempools for stale tx/payload processing.
func (s *Server) initStaleTxMemPool() { func (s *Server) initStaleMemPools() {
cfg := s.chain.GetConfig() cfg := s.chain.GetConfig()
threshold := 5 threshold := 5
if cfg.ValidatorsCount*2 > threshold { if cfg.ValidatorsCount*2 > threshold {
@ -974,6 +1049,9 @@ func (s *Server) initStaleTxMemPool() {
mp := s.chain.GetMemPool() mp := s.chain.GetMemPool()
mp.SetResendThreshold(uint32(threshold), s.broadcastTX) 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 // broadcastTxLoop is a loop for batching and sending

View file

@ -19,6 +19,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/network/capability" "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/network/payload"
"github.com/nspcc-dev/neo-go/pkg/util" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/atomic" "go.uber.org/atomic"
@ -445,7 +447,7 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType,
p.handshaked = true p.handshaked = true
p.messageHandler = func(t *testing.T, msg *Message) { p.messageHandler = func(t *testing.T, msg *Message) {
switch msg.Command { switch msg.Command {
case CMDTX, CMDBlock, CMDConsensus: case CMDTX, CMDBlock, CMDConsensus, CMDP2PNotaryRequest:
require.Equal(t, found, msg.Payload) require.Equal(t, found, msg.Payload)
recvResponse.Store(true) recvResponse.Store(true)
case CMDNotFound: case CMDNotFound:
@ -463,6 +465,7 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType,
func TestGetData(t *testing.T) { func TestGetData(t *testing.T) {
s, shutdown := startTestServer(t) s, shutdown := startTestServer(t)
defer shutdown() defer shutdown()
s.chain.(*testChain).utilityTokenBalance = big.NewInt(1000000)
t.Run("block", func(t *testing.T) { t.Run("block", func(t *testing.T) {
b := newDummyBlock(2, 0) b := newDummyBlock(2, 0)
@ -478,6 +481,44 @@ func TestGetData(t *testing.T) {
notFound := []util.Uint256{hs[0], hs[2]} notFound := []util.Uint256{hs[0], hs[2]}
s.testHandleGetData(t, payload.TXType, hs, notFound, tx) 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) { func initGetBlocksTest(t *testing.T) (*Server, func(), []*block.Block) {
@ -602,6 +643,7 @@ func TestGetHeaders(t *testing.T) {
func TestInv(t *testing.T) { func TestInv(t *testing.T) {
s, shutdown := startTestServer(t) s, shutdown := startTestServer(t)
defer shutdown() defer shutdown()
s.chain.(*testChain).utilityTokenBalance = big.NewInt(10000000)
var actual []util.Uint256 var actual []util.Uint256
p := newLocalPeer(t, s) p := newLocalPeer(t, s)
@ -632,6 +674,24 @@ func TestInv(t *testing.T) {
}) })
require.Equal(t, []util.Uint256{hs[0], hs[2]}, actual) 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) { func TestRequestTx(t *testing.T) {
@ -763,3 +823,43 @@ func TestMemPool(t *testing.T) {
s.testHandleMessage(t, p, CMDMempool, payload.NullPayload{}) s.testHandleMessage(t, p, CMDMempool, payload.NullPayload{})
require.ElementsMatch(t, expected, actual) 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))
})
}