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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
@ -43,6 +44,7 @@ const (
version = "0.1.0"
defaultMemPoolSize = 50000
defaultP2PNotaryRequestPayloadPoolSize = 1000
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
verificationGasLimit = 100000000 // 1 GAS
)
@ -116,6 +118,10 @@ type Blockchain struct {
memPool *mempool.Pool
// postBlock is a set of callback methods which should be run under the Blockchain lock after new block is persisted.
// Block's transactions are passed via mempool.
postBlock []func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)
sbCommittee keys.PublicKeys
log *zap.Logger
@ -151,6 +157,10 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
cfg.MemPoolSize = defaultMemPoolSize
log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize))
}
if cfg.P2PSigExtensions && cfg.P2PNotaryRequestPayloadPoolSize <= 0 {
cfg.P2PNotaryRequestPayloadPoolSize = defaultP2PNotaryRequestPayloadPoolSize
log.Info("P2PNotaryRequestPayloadPool size is not set or wrong, setting default value", zap.Int("P2PNotaryRequestPayloadPoolSize", cfg.P2PNotaryRequestPayloadPoolSize))
}
if cfg.MaxTraceableBlocks == 0 {
cfg.MaxTraceableBlocks = defaultMaxTraceableBlocks
log.Info("MaxTraceableBlocks is not set or wrong, using default value", zap.Uint32("MaxTraceableBlocks", cfg.MaxTraceableBlocks))
@ -164,7 +174,7 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
dao: dao.NewSimple(s, cfg.Magic, cfg.StateRootInHeader),
stopCh: make(chan struct{}),
runToExitCh: make(chan struct{}),
memPool: mempool.New(cfg.MemPoolSize),
memPool: mempool.New(cfg.MemPoolSize, 0),
sbCommittee: committee,
log: log,
events: make(chan bcEvent),
@ -452,7 +462,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error {
if !block.MerkleRoot.Equals(merkle) {
return errors.New("invalid block: MerkleRoot mismatch")
}
mp = mempool.New(len(block.Transactions))
mp = mempool.New(len(block.Transactions), 0)
for _, tx := range block.Transactions {
var err error
// Transactions are verified before adding them
@ -464,7 +474,7 @@ func (bc *Blockchain) AddBlock(block *block.Block) error {
continue
}
} else {
err = bc.verifyAndPoolTx(tx, mp)
err = bc.verifyAndPoolTx(tx, mp, bc)
}
if err != nil && bc.config.VerifyTransactions {
return fmt.Errorf("transaction %s failed to verify: %w", tx.Hash().StringLE(), err)
@ -720,6 +730,13 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
bc.lock.Unlock()
return fmt.Errorf("failed to call OnPersistEnd for Policy native contract: %w", err)
}
if bc.P2PSigExtensionsEnabled() {
err := bc.contracts.Notary.OnPersistEnd(bc.dao)
if err != nil {
bc.lock.Unlock()
return fmt.Errorf("failed to call OnPersistEnd for Notary native contract: %w", err)
}
}
if err := bc.contracts.Designate.OnPersistEnd(bc.dao); err != nil {
bc.lock.Unlock()
return err
@ -733,7 +750,10 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
}
bc.topBlock.Store(block)
atomic.StoreUint32(&bc.blockHeight, block.Index)
bc.memPool.RemoveStale(func(tx *transaction.Transaction) bool { return bc.isTxStillRelevant(tx, txpool) }, bc)
bc.memPool.RemoveStale(func(tx *transaction.Transaction) bool { return bc.IsTxStillRelevant(tx, txpool, false) }, bc)
for _, f := range bc.postBlock {
f(bc, txpool, block)
}
bc.lock.Unlock()
updateBlockHeightMetric(block.Index)
@ -934,6 +954,24 @@ func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint
return &neo.Balance, neo.LastUpdatedBlock
}
// GetNotaryBalance returns Notary deposit amount for the specified account.
func (bc *Blockchain) GetNotaryBalance(acc util.Uint160) *big.Int {
return bc.contracts.Notary.BalanceOf(bc.dao, acc)
}
// GetNotaryContractScriptHash returns Notary native contract hash.
func (bc *Blockchain) GetNotaryContractScriptHash() util.Uint160 {
if bc.P2PSigExtensionsEnabled() {
return bc.contracts.Notary.Hash
}
return util.Uint160{}
}
// GetNotaryDepositExpiration returns Notary deposit expiration height for the specified account.
func (bc *Blockchain) GetNotaryDepositExpiration(acc util.Uint160) uint32 {
return bc.contracts.Notary.ExpirationOf(bc.dao, acc)
}
// LastBatch returns last persisted storage batch.
func (bc *Blockchain) LastBatch() *storage.MemBatch {
return bc.lastBatch
@ -1204,16 +1242,6 @@ func (bc *Blockchain) FeePerByte() int64 {
return bc.contracts.Policy.GetFeePerByteInternal(bc.dao)
}
// GetMaxBlockSize returns maximum allowed block size from native Policy contract.
func (bc *Blockchain) GetMaxBlockSize() uint32 {
return bc.contracts.Policy.GetMaxBlockSizeInternal(bc.dao)
}
// GetMaxBlockSystemFee returns maximum block system fee from native Policy contract.
func (bc *Blockchain) GetMaxBlockSystemFee() int64 {
return bc.contracts.Policy.GetMaxBlockSystemFeeInternal(bc.dao)
}
// GetMemPool returns the memory pool of the blockchain.
func (bc *Blockchain) GetMemPool() *mempool.Pool {
return bc.memPool
@ -1279,9 +1307,10 @@ var (
// verifyAndPoolTx verifies whether a transaction is bonafide or not and tries
// to add it to the mempool given.
func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.Pool) error {
func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.Pool, feer mempool.Feer, data ...interface{}) error {
height := bc.BlockHeight()
if t.ValidUntilBlock <= height || t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement {
isPartialTx := data != nil
if t.ValidUntilBlock <= height || !isPartialTx && t.ValidUntilBlock > height+transaction.MaxValidUntilBlockIncrement {
return fmt.Errorf("%w: ValidUntilBlock = %d, current height = %d", ErrTxExpired, t.ValidUntilBlock, height)
}
// Policying.
@ -1316,14 +1345,14 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return err
}
}
err := bc.verifyTxWitnesses(t, nil)
err := bc.verifyTxWitnesses(t, nil, isPartialTx)
if err != nil {
return err
}
if err := bc.verifyTxAttributes(t); err != nil {
if err := bc.verifyTxAttributes(t, isPartialTx); err != nil {
return err
}
err = pool.Add(t, bc)
err = pool.Add(t, feer, data...)
if err != nil {
switch {
case errors.Is(err, mempool.ErrConflict):
@ -1344,7 +1373,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return nil
}
func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction, isPartialTx bool) error {
for i := range tx.Attributes {
switch attrType := tx.Attributes[i].Type; attrType {
case transaction.HighPriority:
@ -1384,9 +1413,19 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
if !bc.config.P2PSigExtensions {
return fmt.Errorf("%w: NotValidBefore attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
}
nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore)
if height := bc.BlockHeight(); height < nvb.Height {
return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb.Height, height)
nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore).Height
if isPartialTx {
maxNVBDelta := bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)
if bc.BlockHeight()+maxNVBDelta < nvb {
return fmt.Errorf("%w: partially-filled transaction should become valid not less then %d blocks after current chain's height %d", ErrInvalidAttribute, maxNVBDelta, bc.BlockHeight())
}
if nvb+maxNVBDelta < tx.ValidUntilBlock {
return fmt.Errorf("%w: partially-filled transaction should be valid during less than %d blocks", ErrInvalidAttribute, maxNVBDelta)
}
} else {
if height := bc.BlockHeight(); height < nvb {
return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb, height)
}
}
case transaction.ConflictsT:
if !bc.config.P2PSigExtensions {
@ -1412,13 +1451,13 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error {
return nil
}
// isTxStillRelevant is a callback for mempool transaction filtering after the
// IsTxStillRelevant is a callback for mempool transaction filtering after the
// new block addition. It returns false for transactions added by the new block
// (passed via txpool) and does witness reverification for non-standard
// contracts. It operates under the assumption that full transaction verification
// was already done so we don't need to check basic things like size, input/output
// correctness, presence in blocks before the new one, etc.
func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool) bool {
func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool {
var recheckWitness bool
var curheight = bc.BlockHeight()
@ -1432,7 +1471,7 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *memp
} else if txpool.HasConflicts(t, bc) {
return false
}
if err := bc.verifyTxAttributes(t); err != nil {
if err := bc.verifyTxAttributes(t, isPartialTx); err != nil {
return false
}
for i := range t.Scripts {
@ -1442,7 +1481,7 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txpool *memp
}
}
if recheckWitness {
return bc.verifyTxWitnesses(t, nil) == nil
return bc.verifyTxWitnesses(t, nil, isPartialTx) == nil
}
return true
@ -1525,10 +1564,10 @@ func (bc *Blockchain) verifyStateRootWitness(r *state.MPTRoot) error {
// current blockchain state. Note that this verification is completely isolated
// from the main node's mempool.
func (bc *Blockchain) VerifyTx(t *transaction.Transaction) error {
var mp = mempool.New(1)
var mp = mempool.New(1, 0)
bc.lock.RLock()
defer bc.lock.RUnlock()
return bc.verifyAndPoolTx(t, mp)
return bc.verifyAndPoolTx(t, mp, bc)
}
// PoolTx verifies and tries to add given transaction into the mempool. If not
@ -1545,7 +1584,21 @@ func (bc *Blockchain) PoolTx(t *transaction.Transaction, pools ...*mempool.Pool)
if len(pools) == 1 {
pool = pools[0]
}
return bc.verifyAndPoolTx(t, pool)
return bc.verifyAndPoolTx(t, pool, bc)
}
// PoolTxWithData verifies and tries to add given transaction with additional data into the mempool.
func (bc *Blockchain) PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc blockchainer.Blockchainer, tx *transaction.Transaction, data interface{}) error) error {
bc.lock.RLock()
defer bc.lock.RUnlock()
if verificationFunction != nil {
err := verificationFunction(bc, t, data)
if err != nil {
return err
}
}
return bc.verifyAndPoolTx(t, mp, feer, data)
}
//GetStandByValidators returns validators from the configuration.
@ -1595,6 +1648,7 @@ var (
ErrWitnessHashMismatch = errors.New("witness hash mismatch")
ErrNativeContractWitness = errors.New("native contract witness must have empty verification script")
ErrVerificationFailed = errors.New("signature check failed")
ErrInvalidSignature = fmt.Errorf("%w: invalid signature", ErrVerificationFailed)
ErrUnknownVerificationContract = errors.New("unknown verification contract")
ErrInvalidVerificationContract = errors.New("verification contract is missing `verify` method")
)
@ -1670,12 +1724,12 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
if err != nil {
return 0, fmt.Errorf("%w: invalid return value", ErrVerificationFailed)
}
if !res {
return 0, fmt.Errorf("%w: invalid signature", ErrVerificationFailed)
}
if vm.Estack().Len() != 0 {
return 0, fmt.Errorf("%w: expected exactly one returned value", ErrVerificationFailed)
}
if !res {
return vm.GasConsumed(), ErrInvalidSignature
}
} else {
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
// not yet added into any block.
// Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87).
func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block) error {
func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block, isPartialTx bool) error {
if len(t.Signers) != len(t.Scripts) {
return fmt.Errorf("%w: %d vs %d", ErrTxInvalidWitnessNum, len(t.Signers), len(t.Scripts))
}
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t)
gasLimit := t.NetworkFee - int64(t.Size())*bc.FeePerByte()
if bc.P2PSigExtensionsEnabled() {
attrs := t.GetAttributes(transaction.NotaryAssistedT)
if len(attrs) != 0 {
na := attrs[0].Value.(*transaction.NotaryAssisted)
gasLimit -= (int64(na.NKeys) + 1) * transaction.NotaryServiceFeePerKey
}
}
for i := range t.Signers {
gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit)
if err != nil {
if err != nil &&
!(i == 0 && isPartialTx && errors.Is(err, ErrInvalidSignature)) { // it's OK for partially-filled transaction with dummy first witness.
return fmt.Errorf("witness #%d: %w", i, err)
}
gasLimit -= gasConsumed
@ -1749,3 +1811,33 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block *
func (bc *Blockchain) P2PSigExtensionsEnabled() bool {
return bc.config.P2PSigExtensions
}
// RegisterPostBlock appends provided function to the list of functions which should be run after new block
// is stored.
func (bc *Blockchain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) {
bc.postBlock = append(bc.postBlock, f)
}
// -- start Policer.
// GetPolicer provides access to policy values via Policer interface.
func (bc *Blockchain) GetPolicer() blockchainer.Policer {
return bc
}
// GetMaxBlockSize returns maximum allowed block size from native Policy contract.
func (bc *Blockchain) GetMaxBlockSize() uint32 {
return bc.contracts.Policy.GetMaxBlockSizeInternal(bc.dao)
}
// GetMaxBlockSystemFee returns maximum block system fee from native Policy contract.
func (bc *Blockchain) GetMaxBlockSystemFee() int64 {
return bc.contracts.Policy.GetMaxBlockSystemFeeInternal(bc.dao)
}
// GetMaxVerificationGAS returns maximum verification GAS Policy limit.
func (bc *Blockchain) GetMaxVerificationGAS() int64 {
return bc.contracts.Policy.GetMaxVerificationGas(bc.dao)
}
// -- end Policer.

View file

@ -14,6 +14,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
@ -447,7 +448,7 @@ func TestVerifyTx(t *testing.T) {
require.True(t, errors.Is(err, ErrAlreadyExists))
})
t.Run("MemPoolOOM", func(t *testing.T) {
bc.memPool = mempool.New(1)
bc.memPool = mempool.New(1, 0)
tx1 := bc.newTestTx(h, testScript)
tx1.NetworkFee += 10000 // Give it more priority.
require.NoError(t, accs[0].SignTx(tx1))
@ -926,6 +927,96 @@ func TestVerifyTx(t *testing.T) {
})
})
})
t.Run("Partially-filled transaction", func(t *testing.T) {
bc.config.P2PSigExtensions = true
getPartiallyFilledTx := func(nvb uint32, validUntil uint32) *transaction.Transaction {
tx := bc.newTestTx(h, testScript)
tx.ValidUntilBlock = validUntil
tx.Attributes = []transaction.Attribute{
{
Type: transaction.NotValidBeforeT,
Value: &transaction.NotValidBefore{Height: nvb},
},
{
Type: transaction.NotaryAssistedT,
Value: &transaction.NotaryAssisted{NKeys: 0},
},
}
tx.Signers = []transaction.Signer{
{
Account: bc.contracts.Notary.Hash,
Scopes: transaction.None,
},
{
Account: testchain.MultisigScriptHash(),
Scopes: transaction.None,
},
}
size := io.GetVarSize(tx)
netFee, sizeDelta := fee.Calculate(testchain.MultisigVerificationScript())
tx.NetworkFee = netFee + // multisig witness verification price
int64(size)*bc.FeePerByte() + // fee for unsigned size
int64(sizeDelta)*bc.FeePerByte() + //fee for multisig size
66*bc.FeePerByte() + // fee for Notary signature size (66 bytes for Invocation script and 0 bytes for Verification script)
2*bc.FeePerByte() + // fee for the length of each script in Notary witness (they are nil, so we did not take them into account during `size` calculation)
transaction.NotaryServiceFeePerKey + // fee for Notary attribute
fee.Opcode( // Notary verification script
opcode.PUSHDATA1, opcode.RET, // invocation script
opcode.DEPTH, opcode.PACK, opcode.PUSHDATA1, opcode.RET, // arguments for native verification call
opcode.PUSHDATA1, opcode.SYSCALL, opcode.RET) + // Neo.Native.Call
native.NotaryVerificationPrice // Notary witness verification price
tx.Scripts = []transaction.Witness{
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...),
VerificationScript: []byte{},
},
{
InvocationScript: testchain.Sign(tx.GetSignedPart()),
VerificationScript: testchain.MultisigVerificationScript(),
},
}
return tx
}
mp := mempool.New(10, 1)
verificationF := func(bc blockchainer.Blockchainer, tx *transaction.Transaction, data interface{}) error {
if data.(int) > 5 {
return errors.New("bad data")
}
return nil
}
t.Run("failed pre-verification", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1)
require.Error(t, bc.PoolTxWithData(tx, 6, mp, bc, verificationF)) // here and below let's use `bc` instead of proper NotaryFeer for the test simplicity.
})
t.Run("GasLimitExceeded during witness verification", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1)
tx.NetworkFee-- // to check that NetworkFee was set correctly in getPartiallyFilledTx
tx.Scripts = []transaction.Witness{
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...),
VerificationScript: []byte{},
},
{
InvocationScript: testchain.Sign(tx.GetSignedPart()),
VerificationScript: testchain.MultisigVerificationScript(),
},
}
require.Error(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF))
})
t.Run("bad NVB: too big", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1, bc.blockHeight+1)
require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute))
})
t.Run("bad ValidUntilBlock: too small", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao)+1)
require.True(t, errors.Is(bc.PoolTxWithData(tx, 5, mp, bc, verificationF), ErrInvalidAttribute))
})
t.Run("good", func(t *testing.T) {
tx := getPartiallyFilledTx(bc.blockHeight, bc.blockHeight+1)
require.NoError(t, bc.PoolTxWithData(tx, 5, mp, bc, verificationF))
})
})
}
func TestVerifyHashAgainstScript(t *testing.T) {
@ -1021,9 +1112,9 @@ func TestIsTxStillRelevant(t *testing.T) {
tx := newTx(t)
require.NoError(t, testchain.SignTx(bc, tx))
require.True(t, bc.isTxStillRelevant(tx, nil))
require.True(t, bc.IsTxStillRelevant(tx, nil, false))
require.NoError(t, bc.AddBlock(bc.newBlock()))
require.False(t, bc.isTxStillRelevant(tx, nil))
require.False(t, bc.IsTxStillRelevant(tx, nil, false))
})
t.Run("tx is already persisted", func(t *testing.T) {
@ -1031,9 +1122,9 @@ func TestIsTxStillRelevant(t *testing.T) {
tx.ValidUntilBlock = bc.BlockHeight() + 2
require.NoError(t, testchain.SignTx(bc, tx))
require.True(t, bc.isTxStillRelevant(tx, nil))
require.True(t, bc.IsTxStillRelevant(tx, nil, false))
require.NoError(t, bc.AddBlock(bc.newBlock(tx)))
require.False(t, bc.isTxStillRelevant(tx, nil))
require.False(t, bc.IsTxStillRelevant(tx, nil, false))
})
t.Run("tx with Conflicts attribute", func(t *testing.T) {
@ -1047,9 +1138,9 @@ func TestIsTxStillRelevant(t *testing.T) {
}}
require.NoError(t, testchain.SignTx(bc, tx2))
require.True(t, bc.isTxStillRelevant(tx1, mp))
require.NoError(t, bc.verifyAndPoolTx(tx2, mp))
require.False(t, bc.isTxStillRelevant(tx1, mp))
require.True(t, bc.IsTxStillRelevant(tx1, mp, false))
require.NoError(t, bc.verifyAndPoolTx(tx2, mp, bc))
require.False(t, bc.IsTxStillRelevant(tx1, mp, false))
})
t.Run("NotValidBefore", func(t *testing.T) {
tx3 := newTx(t)
@ -1060,9 +1151,9 @@ func TestIsTxStillRelevant(t *testing.T) {
tx3.ValidUntilBlock = bc.BlockHeight() + 2
require.NoError(t, testchain.SignTx(bc, tx3))
require.False(t, bc.isTxStillRelevant(tx3, nil))
require.False(t, bc.IsTxStillRelevant(tx3, nil, false))
require.NoError(t, bc.AddBlock(bc.newBlock()))
require.True(t, bc.isTxStillRelevant(tx3, nil))
require.True(t, bc.IsTxStillRelevant(tx3, nil, false))
})
t.Run("contract witness check fails", func(t *testing.T) {
src := fmt.Sprintf(`package verify
@ -1087,9 +1178,9 @@ func TestIsTxStillRelevant(t *testing.T) {
require.NoError(t, testchain.SignTx(bc, tx))
tx.Scripts = append(tx.Scripts, transaction.Witness{})
require.True(t, bc.isTxStillRelevant(tx, mp))
require.True(t, bc.IsTxStillRelevant(tx, mp, false))
require.NoError(t, bc.AddBlock(bc.newBlock()))
require.False(t, bc.isTxStillRelevant(tx, mp))
require.False(t, bc.IsTxStillRelevant(tx, mp, false))
})
}

View file

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

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
}
func invokeContractMethodBy(t *testing.T, chain *Blockchain, signer *wallet.Account, hash util.Uint160, method string, args ...interface{}) (*state.AppExecResult, error) {
var (
netfee int64 = 1000_0000
sysfee int64 = 1_0000_0000
)
transferTx := transferTokenFromMultisigAccount(t, chain, signer.PrivateKey().PublicKey().GetScriptHash(), chain.contracts.GAS.Hash, sysfee+netfee+1000_0000, nil)
res, err := chain.GetAppExecResults(transferTx.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, vm.HaltState, res[0].VMState)
require.Equal(t, 0, len(res[0].Stack))
w := io.NewBufBinWriter()
emit.AppCallWithOperationAndArgs(w.BinWriter, hash, method, args...)
if w.Err != nil {
return nil, w.Err
}
script := w.Bytes()
tx := transaction.New(chain.GetConfig().Magic, script, sysfee)
tx.ValidUntilBlock = chain.blockHeight + 1
tx.Signers = []transaction.Signer{
{Account: signer.PrivateKey().PublicKey().GetScriptHash()},
}
tx.NetworkFee = netfee
err = signer.SignTx(tx)
require.NoError(t, err)
require.NoError(t, chain.AddBlock(chain.newBlock(tx)))
res, err = chain.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
return &res[0], nil
}
func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, tokenHash util.Uint160, amount int64, additionalArgs ...interface{}) *transaction.Transaction {
transferTx := newNEP17Transfer(tokenHash, testchain.MultisigScriptHash(), to, amount, additionalArgs...)
transferTx.SystemFee = 100000000
@ -451,7 +483,7 @@ func transferTokenFromMultisigAccount(t *testing.T, chain *Blockchain, to, token
}
func checkResult(t *testing.T, result *state.AppExecResult, expected stackitem.Item) {
require.Equal(t, vm.HaltState, result.VMState)
require.Equal(t, vm.HaltState, result.VMState, result.FaultException)
require.Equal(t, 1, len(result.Stack))
require.Equal(t, expected, result.Stack[0])
}

View file

@ -39,6 +39,7 @@ var (
type item struct {
txn *transaction.Transaction
blockStamp uint32
data interface{}
}
// items is a slice of item.
@ -64,9 +65,10 @@ type Pool struct {
capacity int
feePerByte int64
payerIndex int
resendThreshold uint32
resendFunc func(*transaction.Transaction)
resendFunc func(*transaction.Transaction, interface{})
}
func (p items) Len() int { return len(p) }
@ -149,11 +151,12 @@ func (mp *Pool) HasConflicts(t *transaction.Transaction, fee Feer) bool {
// tryAddSendersFee tries to add system fee and network fee to the total sender`s fee in mempool
// and returns false if both balance check is required and sender has not enough GAS to pay
func (mp *Pool) tryAddSendersFee(tx *transaction.Transaction, feer Feer, needCheck bool) bool {
senderFee, ok := mp.fees[tx.Sender()]
payer := tx.Signers[mp.payerIndex].Account
senderFee, ok := mp.fees[payer]
if !ok {
senderFee.balance = feer.GetUtilityTokenBalance(tx.Sender())
senderFee.balance = feer.GetUtilityTokenBalance(payer)
senderFee.feeSum = big.NewInt(0)
mp.fees[tx.Sender()] = senderFee
mp.fees[payer] = senderFee
}
if needCheck {
newFeeSum, err := checkBalance(tx, senderFee)
@ -182,11 +185,14 @@ func checkBalance(tx *transaction.Transaction, balance utilityBalanceAndFees) (*
}
// Add tries to add given transaction to the Pool.
func (mp *Pool) Add(t *transaction.Transaction, fee Feer) error {
func (mp *Pool) Add(t *transaction.Transaction, fee Feer, data ...interface{}) error {
var pItem = item{
txn: t,
blockStamp: fee.BlockHeight(),
}
if data != nil {
pItem.data = data[0]
}
mp.lock.Lock()
if mp.containsKey(t.Hash()) {
mp.lock.Unlock()
@ -281,14 +287,16 @@ func (mp *Pool) removeInternal(hash util.Uint256, feer Feer) {
break
}
}
itm := mp.verifiedTxes[num]
if num < len(mp.verifiedTxes)-1 {
mp.verifiedTxes = append(mp.verifiedTxes[:num], mp.verifiedTxes[num+1:]...)
} else if num == len(mp.verifiedTxes)-1 {
mp.verifiedTxes = mp.verifiedTxes[:num]
}
senderFee := mp.fees[tx.Sender()]
payer := itm.txn.Signers[mp.payerIndex].Account
senderFee := mp.fees[payer]
senderFee.feeSum.Sub(senderFee.feeSum, big.NewInt(tx.SystemFee+tx.NetworkFee))
mp.fees[tx.Sender()] = senderFee
mp.fees[payer] = senderFee
if feer.P2PSigExtensionsEnabled() {
// remove all conflicting hashes from mp.conflicts list
mp.removeConflictsOf(tx)
@ -314,7 +322,7 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer)
mp.conflicts = make(map[util.Uint256][]util.Uint256)
}
height := feer.BlockHeight()
var staleTxs []*transaction.Transaction
var staleItems []item
for _, itm := range mp.verifiedTxes {
if isOK(itm.txn) && mp.checkPolicy(itm.txn, policyChanged) && mp.tryAddSendersFee(itm.txn, feer, true) {
newVerifiedTxes = append(newVerifiedTxes, itm)
@ -325,11 +333,11 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer)
}
}
if mp.resendThreshold != 0 {
// tx is resend at resendThreshold, 2*resendThreshold, 4*resendThreshold ...
// item is resend at resendThreshold, 2*resendThreshold, 4*resendThreshold ...
// so quotient must be a power of two.
diff := (height - itm.blockStamp)
if diff%mp.resendThreshold == 0 && bits.OnesCount32(diff/mp.resendThreshold) == 1 {
staleTxs = append(staleTxs, itm.txn)
staleItems = append(staleItems, itm)
}
}
} else {
@ -339,8 +347,8 @@ func (mp *Pool) RemoveStale(isOK func(*transaction.Transaction) bool, feer Feer)
}
}
}
if len(staleTxs) != 0 {
go mp.resendStaleTxs(staleTxs)
if len(staleItems) != 0 {
go mp.resendStaleItems(staleItems)
}
mp.verifiedTxes = newVerifiedTxes
mp.lock.Unlock()
@ -366,11 +374,12 @@ func (mp *Pool) checkPolicy(tx *transaction.Transaction, policyChanged bool) boo
}
// New returns a new Pool struct.
func New(capacity int) *Pool {
func New(capacity int, payerIndex int) *Pool {
return &Pool{
verifiedMap: make(map[util.Uint256]*transaction.Transaction),
verifiedTxes: make([]item, 0, capacity),
capacity: capacity,
payerIndex: payerIndex,
fees: make(map[util.Uint160]utilityBalanceAndFees),
conflicts: make(map[util.Uint256][]util.Uint256),
oracleResp: make(map[uint64]util.Uint256),
@ -379,16 +388,16 @@ func New(capacity int) *Pool {
// SetResendThreshold sets threshold after which transaction will be considered stale
// and returned for retransmission by `GetStaleTransactions`.
func (mp *Pool) SetResendThreshold(h uint32, f func(*transaction.Transaction)) {
func (mp *Pool) SetResendThreshold(h uint32, f func(*transaction.Transaction, interface{})) {
mp.lock.Lock()
defer mp.lock.Unlock()
mp.resendThreshold = h
mp.resendFunc = f
}
func (mp *Pool) resendStaleTxs(txs []*transaction.Transaction) {
for i := range txs {
mp.resendFunc(txs[i])
func (mp *Pool) resendStaleItems(items []item) {
for i := range items {
mp.resendFunc(items[i].txn, items[i].data)
}
}
@ -403,6 +412,30 @@ func (mp *Pool) TryGetValue(hash util.Uint256) (*transaction.Transaction, bool)
return nil, false
}
// TryGetData returns data associated with the specified transaction if it exists in the memory pool.
func (mp *Pool) TryGetData(hash util.Uint256) (interface{}, bool) {
mp.lock.RLock()
defer mp.lock.RUnlock()
if tx, ok := mp.verifiedMap[hash]; ok {
itm := item{txn: tx}
n := sort.Search(len(mp.verifiedTxes), func(n int) bool {
return itm.CompareTo(mp.verifiedTxes[n]) >= 0
})
if n < len(mp.verifiedTxes) {
for i := n; i < len(mp.verifiedTxes); i++ { // items may have equal priority, so `n` is the left bound of the items which are as prioritized as the desired `itm`.
if mp.verifiedTxes[i].txn.Hash() == hash {
return mp.verifiedTxes[i].data, ok
}
if itm.CompareTo(mp.verifiedTxes[i]) != 0 {
break
}
}
}
}
return nil, false
}
// GetVerifiedTransactions returns a slice of transactions with their fees.
func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction {
mp.lock.RLock()
@ -420,9 +453,10 @@ func (mp *Pool) GetVerifiedTransactions() []*transaction.Transaction {
// checkTxConflicts is an internal unprotected version of Verify. It takes into
// consideration conflicting transactions which are about to be removed from mempool.
func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*transaction.Transaction, error) {
actualSenderFee, ok := mp.fees[tx.Sender()]
payer := tx.Signers[mp.payerIndex].Account
actualSenderFee, ok := mp.fees[payer]
if !ok {
actualSenderFee.balance = fee.GetUtilityTokenBalance(tx.Sender())
actualSenderFee.balance = fee.GetUtilityTokenBalance(payer)
actualSenderFee.feeSum = big.NewInt(0)
}
@ -434,7 +468,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
if conflictingHashes, ok := mp.conflicts[tx.Hash()]; ok {
for _, hash := range conflictingHashes {
existingTx := mp.verifiedMap[hash]
if existingTx.HasSigner(tx.Sender()) && existingTx.NetworkFee > tx.NetworkFee {
if existingTx.HasSigner(payer) && existingTx.NetworkFee > tx.NetworkFee {
return nil, fmt.Errorf("%w: conflicting transaction %s has bigger network fee", ErrConflictsAttribute, existingTx.Hash().StringBE())
}
conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx)
@ -447,7 +481,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
if !ok {
continue
}
if !tx.HasSigner(existingTx.Sender()) {
if !tx.HasSigner(existingTx.Signers[mp.payerIndex].Account) {
return nil, fmt.Errorf("%w: not signed by the sender of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE())
}
if existingTx.NetworkFee >= tx.NetworkFee {
@ -461,7 +495,7 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
feeSum: new(big.Int).Set(actualSenderFee.feeSum),
}
for _, conflictingTx := range conflictsToBeRemoved {
if conflictingTx.Sender().Equals(tx.Sender()) {
if conflictingTx.Signers[mp.payerIndex].Account.Equals(payer) {
expectedSenderFee.feeSum.Sub(expectedSenderFee.feeSum, big.NewInt(conflictingTx.SystemFee+conflictingTx.NetworkFee))
}
}

View file

@ -10,6 +10,7 @@ import (
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/assert"
@ -20,10 +21,9 @@ type FeerStub struct {
feePerByte int64
p2pSigExt bool
blockHeight uint32
balance int64
}
var balance = big.NewInt(10000000)
func (fs *FeerStub) FeePerByte() int64 {
return fs.feePerByte
}
@ -33,7 +33,7 @@ func (fs *FeerStub) BlockHeight() uint32 {
}
func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
return balance
return big.NewInt(fs.balance)
}
func (fs *FeerStub) P2PSigExtensionsEnabled() bool {
@ -41,7 +41,7 @@ func (fs *FeerStub) P2PSigExtensionsEnabled() bool {
}
func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) {
mp := New(10)
mp := New(10, 0)
tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx.Nonce = 0
tx.Signers = []transaction.Signer{{Account: util.Uint160{1, 2, 3}}}
@ -62,7 +62,7 @@ func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) {
}
func TestMemPoolRemoveStale(t *testing.T) {
mp := New(5)
mp := New(5, 0)
txs := make([]*transaction.Transaction, 5)
for i := range txs {
txs[i] = transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
@ -72,7 +72,7 @@ func TestMemPoolRemoveStale(t *testing.T) {
}
staleTxs := make(chan *transaction.Transaction, 5)
f := func(tx *transaction.Transaction) {
f := func(tx *transaction.Transaction, _ interface{}) {
staleTxs <- tx
}
mp.SetResendThreshold(5, f)
@ -111,9 +111,9 @@ func TestMemPoolAddRemove(t *testing.T) {
}
func TestOverCapacity(t *testing.T) {
var fs = &FeerStub{}
var fs = &FeerStub{balance: 10000000}
const mempoolSize = 10
mp := New(mempoolSize)
mp := New(mempoolSize, 0)
for i := 0; i < mempoolSize; i++ {
tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
@ -186,7 +186,7 @@ func TestOverCapacity(t *testing.T) {
func TestGetVerified(t *testing.T) {
var fs = &FeerStub{}
const mempoolSize = 10
mp := New(mempoolSize)
mp := New(mempoolSize, 0)
txes := make([]*transaction.Transaction, 0, mempoolSize)
for i := 0; i < mempoolSize; i++ {
@ -210,7 +210,7 @@ func TestGetVerified(t *testing.T) {
func TestRemoveStale(t *testing.T) {
var fs = &FeerStub{}
const mempoolSize = 10
mp := New(mempoolSize)
mp := New(mempoolSize, 0)
txes1 := make([]*transaction.Transaction, 0, mempoolSize/2)
txes2 := make([]*transaction.Transaction, 0, mempoolSize/2)
@ -243,50 +243,51 @@ func TestRemoveStale(t *testing.T) {
}
func TestMemPoolFees(t *testing.T) {
mp := New(10)
mp := New(10, 0)
fs := &FeerStub{balance: 10000000}
sender0 := util.Uint160{1, 2, 3}
tx0 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx0.NetworkFee = balance.Int64() + 1
tx0.NetworkFee = fs.balance + 1
tx0.Signers = []transaction.Signer{{Account: sender0}}
// insufficient funds to add transaction, and balance shouldn't be stored
require.Equal(t, false, mp.Verify(tx0, &FeerStub{}))
require.Error(t, mp.Add(tx0, &FeerStub{}))
require.Equal(t, false, mp.Verify(tx0, fs))
require.Error(t, mp.Add(tx0, fs))
require.Equal(t, 0, len(mp.fees))
balancePart := new(big.Int).Div(balance, big.NewInt(4))
balancePart := new(big.Int).Div(big.NewInt(fs.balance), big.NewInt(4))
// no problems with adding another transaction with lower fee
tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx1.NetworkFee = balancePart.Int64()
tx1.Signers = []transaction.Signer{{Account: sender0}}
require.NoError(t, mp.Add(tx1, &FeerStub{}))
require.NoError(t, mp.Add(tx1, fs))
require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{
balance: balance,
balance: big.NewInt(fs.balance),
feeSum: big.NewInt(tx1.NetworkFee),
}, mp.fees[sender0])
// balance shouldn't change after adding one more transaction
tx2 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx2.NetworkFee = new(big.Int).Sub(balance, balancePart).Int64()
tx2.NetworkFee = new(big.Int).Sub(big.NewInt(fs.balance), balancePart).Int64()
tx2.Signers = []transaction.Signer{{Account: sender0}}
require.NoError(t, mp.Add(tx2, &FeerStub{}))
require.NoError(t, mp.Add(tx2, fs))
require.Equal(t, 2, len(mp.verifiedTxes))
require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{
balance: balance,
feeSum: balance,
balance: big.NewInt(fs.balance),
feeSum: big.NewInt(fs.balance),
}, mp.fees[sender0])
// can't add more transactions as we don't have enough GAS
tx3 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx3.NetworkFee = 1
tx3.Signers = []transaction.Signer{{Account: sender0}}
require.Equal(t, false, mp.Verify(tx3, &FeerStub{}))
require.Error(t, mp.Add(tx3, &FeerStub{}))
require.Equal(t, false, mp.Verify(tx3, fs))
require.Error(t, mp.Add(tx3, fs))
require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{
balance: balance,
feeSum: balance,
balance: big.NewInt(fs.balance),
feeSum: big.NewInt(fs.balance),
}, mp.fees[sender0])
// check whether sender's fee updates correctly
@ -295,10 +296,10 @@ func TestMemPoolFees(t *testing.T) {
return true
}
return false
}, &FeerStub{})
}, fs)
require.Equal(t, 1, len(mp.fees))
require.Equal(t, utilityBalanceAndFees{
balance: balance,
balance: big.NewInt(fs.balance),
feeSum: big.NewInt(tx2.NetworkFee),
}, mp.fees[sender0])
@ -308,12 +309,13 @@ func TestMemPoolFees(t *testing.T) {
return true
}
return false
}, &FeerStub{})
}, fs)
require.Equal(t, 0, len(mp.fees))
}
func TestMempoolItemsOrder(t *testing.T) {
sender0 := util.Uint160{1, 2, 3}
balance := big.NewInt(10000000)
tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx1.NetworkFee = new(big.Int).Div(balance, big.NewInt(8)).Int64()
@ -352,9 +354,9 @@ func TestMempoolItemsOrder(t *testing.T) {
}
func TestMempoolAddRemoveOracleResponse(t *testing.T) {
mp := New(5)
mp := New(5, 0)
nonce := uint32(0)
fs := &FeerStub{}
fs := &FeerStub{balance: 10000}
newTx := func(netFee int64, id uint64) *transaction.Transaction {
tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx.NetworkFee = netFee
@ -406,9 +408,9 @@ func TestMempoolAddRemoveOracleResponse(t *testing.T) {
func TestMempoolAddRemoveConflicts(t *testing.T) {
capacity := 6
mp := New(capacity)
mp := New(capacity, 0)
var (
fs = &FeerStub{p2pSigExt: true}
fs = &FeerStub{p2pSigExt: true, balance: 100000}
nonce uint32 = 1
)
getConflictsTx := func(netFee int64, hashes ...util.Uint256) *transaction.Transaction {
@ -524,3 +526,116 @@ func TestMempoolAddRemoveConflicts(t *testing.T) {
require.Equal(t, false, ok)
require.True(t, errors.Is(mp.Add(tx13, fs), ErrConflictsAttribute))
}
func TestMempoolAddWithDataGetData(t *testing.T) {
var (
smallNetFee int64 = 3
nonce uint32
)
fs := &FeerStub{
feePerByte: 0,
p2pSigExt: true,
blockHeight: 5,
balance: 100,
}
mp := New(10, 1)
newTx := func(t *testing.T, netFee int64) *transaction.Transaction {
tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.RET)}, 0)
tx.Signers = []transaction.Signer{{}, {}}
tx.NetworkFee = netFee
nonce++
tx.Nonce = nonce
return tx
}
// bad, insufficient deposit
r1 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, fs.balance+1),
}
require.True(t, errors.Is(mp.Add(r1.FallbackTransaction, fs, r1), ErrInsufficientFunds))
// good
r2 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
}
require.NoError(t, mp.Add(r2.FallbackTransaction, fs, r2))
require.True(t, mp.ContainsKey(r2.FallbackTransaction.Hash()))
data, ok := mp.TryGetData(r2.FallbackTransaction.Hash())
require.True(t, ok)
require.Equal(t, r2, data)
// bad, already in pool
require.True(t, errors.Is(mp.Add(r2.FallbackTransaction, fs, r2), ErrDup))
// good, higher priority than r2. The resulting mp.verifiedTxes: [r3, r2]
r3 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee+1),
}
require.NoError(t, mp.Add(r3.FallbackTransaction, fs, r3))
require.True(t, mp.ContainsKey(r3.FallbackTransaction.Hash()))
data, ok = mp.TryGetData(r3.FallbackTransaction.Hash())
require.True(t, ok)
require.Equal(t, r3, data)
// good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4]
r4 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
}
require.NoError(t, mp.Add(r4.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r4.FallbackTransaction.Hash()))
data, ok = mp.TryGetData(r4.FallbackTransaction.Hash())
require.True(t, ok)
require.Equal(t, r4, data)
// good, same priority as r2. The resulting mp.verifiedTxes: [r3, r2, r4, r5]
r5 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
}
require.NoError(t, mp.Add(r5.FallbackTransaction, fs, r5))
require.True(t, mp.ContainsKey(r5.FallbackTransaction.Hash()))
data, ok = mp.TryGetData(r5.FallbackTransaction.Hash())
require.True(t, ok)
require.Equal(t, r5, data)
// and both r2's and r4's data should still be reachable
data, ok = mp.TryGetData(r2.FallbackTransaction.Hash())
require.True(t, ok)
require.Equal(t, r2, data)
data, ok = mp.TryGetData(r4.FallbackTransaction.Hash())
require.True(t, ok)
require.Equal(t, r4, data)
// should fail to get unexisting data
_, ok = mp.TryGetData(util.Uint256{0, 0, 0})
require.False(t, ok)
// but getting nil data is OK. The resulting mp.verifiedTxes: [r3, r2, r4, r5, r6]
r6 := newTx(t, smallNetFee)
require.NoError(t, mp.Add(r6, fs, nil))
require.True(t, mp.ContainsKey(r6.Hash()))
data, ok = mp.TryGetData(r6.Hash())
require.True(t, ok)
require.Nil(t, data)
// getting data: item is in verifiedMap, but not in verifiedTxes
r7 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee),
}
require.NoError(t, mp.Add(r7.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r7.FallbackTransaction.Hash()))
r8 := &payload.P2PNotaryRequest{
MainTransaction: newTx(t, 0),
FallbackTransaction: newTx(t, smallNetFee-1),
}
require.NoError(t, mp.Add(r8.FallbackTransaction, fs, r4))
require.True(t, mp.ContainsKey(r8.FallbackTransaction.Hash()))
mp.verifiedTxes = append(mp.verifiedTxes[:len(mp.verifiedTxes)-2], mp.verifiedTxes[len(mp.verifiedTxes)-1])
_, ok = mp.TryGetData(r7.FallbackTransaction.Hash())
require.False(t, ok)
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"math/big"
"sync"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
@ -26,17 +27,29 @@ type Notary struct {
interop.ContractMD
GAS *GAS
Desig *Designate
lock sync.RWMutex
// isValid defies whether cached values were changed during the current
// consensus iteration. If false, these values will be updated after
// blockchain DAO persisting. If true, we can safely use cached values.
isValid bool
maxNotValidBeforeDelta uint32
}
const (
notaryName = "Notary"
notaryContractID = reservedContractID - 1
// NotaryVerificationPrice is the price of `verify` Notary method.
NotaryVerificationPrice = 100_0000
// prefixDeposit is a prefix for storing Notary deposits.
prefixDeposit = 1
defaultDepositDeltaTill = 5760
defaultMaxNotValidBeforeDelta = 140 // 20 rounds for 7 validators, a little more than half an hour
)
var maxNotValidBeforeDeltaKey = []byte{10}
// newNotary returns Notary native contract.
func newNotary() *Notary {
n := &Notary{ContractMD: *interop.NewContractMD(notaryName)}
@ -76,6 +89,15 @@ func newNotary() *Notary {
md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates)
n.AddMethod(md, desc)
desc = newDescriptor("getMaxNotValidBeforeDelta", smartcontract.IntegerType)
md = newMethodAndPrice(n.getMaxNotValidBeforeDelta, 100_0000, smartcontract.AllowStates)
n.AddMethod(md, desc)
desc = newDescriptor("setMaxNotValidBeforeDelta", smartcontract.BoolType,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(n.setMaxNotValidBeforeDelta, 300_0000, smartcontract.AllowModifyStates)
n.AddMethod(md, desc)
desc = newDescriptor("onPersist", smartcontract.VoidType)
md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates)
n.AddMethod(md, desc)
@ -94,6 +116,8 @@ func (n *Notary) Metadata() *interop.ContractMD {
// Initialize initializes Notary native contract and implements Contract interface.
func (n *Notary) Initialize(ic *interop.Context) error {
n.isValid = true
n.maxNotValidBeforeDelta = defaultMaxNotValidBeforeDelta
return nil
}
@ -116,7 +140,7 @@ func (n *Notary) OnPersist(ic *interop.Context) error {
nFees += int64(nKeys) + 1
if tx.Sender() == n.Hash {
payer := tx.Signers[1]
balance := n.getDepositFor(ic.DAO, payer.Account)
balance := n.GetDepositFor(ic.DAO, payer.Account)
balance.Amount.Sub(balance.Amount, big.NewInt(tx.SystemFee+tx.NetworkFee))
if balance.Amount.Sign() == 0 {
err := n.removeDepositFor(ic.DAO, payer.Account)
@ -142,6 +166,19 @@ func (n *Notary) OnPersist(ic *interop.Context) error {
return nil
}
// OnPersistEnd updates cached Policy values if they've been changed
func (n *Notary) OnPersistEnd(dao dao.DAO) error {
if n.isValid {
return nil
}
n.lock.Lock()
defer n.lock.Unlock()
n.maxNotValidBeforeDelta = getUint32WithKey(n.ContractID, dao, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta)
n.isValid = true
return nil
}
// onPayment records deposited amount as belonging to "from" address with a lock
// till the specified chain's height.
func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
@ -162,7 +199,7 @@ func (n *Notary) onPayment(ic *interop.Context, args []stackitem.Item) stackitem
allowedChangeTill := ic.Tx.Sender() == to
currentHeight := ic.Chain.BlockHeight()
deposit := n.getDepositFor(ic.DAO, to)
deposit := n.GetDepositFor(ic.DAO, to)
till := toUint32(additionalParams[1])
if till < currentHeight {
panic(fmt.Errorf("`till` shouldn't be less then the chain's height %d", currentHeight))
@ -206,7 +243,7 @@ func (n *Notary) lockDepositUntil(ic *interop.Context, args []stackitem.Item) st
if till < ic.Chain.BlockHeight() {
return stackitem.NewBool(false)
}
deposit := n.getDepositFor(ic.DAO, addr)
deposit := n.GetDepositFor(ic.DAO, addr)
if deposit == nil {
return stackitem.NewBool(false)
}
@ -235,7 +272,7 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.
if !args[1].Equals(stackitem.Null{}) {
to = toUint160(args[1])
}
deposit := n.getDepositFor(ic.DAO, from)
deposit := n.GetDepositFor(ic.DAO, from)
if deposit == nil {
return stackitem.NewBool(false)
}
@ -263,21 +300,31 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.
// balanceOf returns deposited GAS amount for specified address.
func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
acc := toUint160(args[0])
deposit := n.getDepositFor(ic.DAO, acc)
if deposit == nil {
return stackitem.NewBigInteger(big.NewInt(0))
return stackitem.NewBigInteger(n.BalanceOf(ic.DAO, acc))
}
return stackitem.NewBigInteger(deposit.Amount)
// BalanceOf is an internal representation of `balanceOf` Notary method.
func (n *Notary) BalanceOf(dao dao.DAO, acc util.Uint160) *big.Int {
deposit := n.GetDepositFor(dao, acc)
if deposit == nil {
return big.NewInt(0)
}
return deposit.Amount
}
// expirationOf Returns deposit lock height for specified address.
func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
acc := toUint160(args[0])
deposit := n.getDepositFor(ic.DAO, acc)
if deposit == nil {
return stackitem.Make(0)
return stackitem.Make(n.ExpirationOf(ic.DAO, acc))
}
return stackitem.Make(deposit.Till)
// ExpirationOf is an internal representation of `expirationOf` Notary method.
func (n *Notary) ExpirationOf(dao dao.DAO, acc util.Uint160) uint32 {
deposit := n.GetDepositFor(dao, acc)
if deposit == nil {
return 0
}
return deposit.Till
}
// verify checks whether the transaction was signed by one of the notaries.
@ -302,7 +349,7 @@ func (n *Notary) verify(ic *interop.Context, args []stackitem.Item) stackitem.It
return stackitem.NewBool(false)
}
payer := tx.Signers[1].Account
balance := n.getDepositFor(ic.DAO, payer)
balance := n.GetDepositFor(ic.DAO, payer)
if balance == nil || balance.Amount.Cmp(big.NewInt(tx.NetworkFee+tx.SystemFee)) < 0 {
return stackitem.NewBool(false)
}
@ -328,9 +375,47 @@ func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) {
return nodes, err
}
// getDepositFor returns state.Deposit for the account specified. It returns nil in case if
// getMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta.
func (n *Notary) getMaxNotValidBeforeDelta(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
return stackitem.NewBigInteger(big.NewInt(int64(n.GetMaxNotValidBeforeDelta(ic.DAO))))
}
// GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method.
func (n *Notary) GetMaxNotValidBeforeDelta(dao dao.DAO) uint32 {
n.lock.RLock()
defer n.lock.RUnlock()
if n.isValid {
return n.maxNotValidBeforeDelta
}
return getUint32WithKey(n.ContractID, dao, maxNotValidBeforeDeltaKey, defaultMaxNotValidBeforeDelta)
}
// setMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta.
func (n *Notary) setMaxNotValidBeforeDelta(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toUint32(args[0])
if value > transaction.MaxValidUntilBlockIncrement/2 || value < uint32(ic.Chain.GetConfig().ValidatorsCount) {
panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be more than %d or less than %d", transaction.MaxValidUntilBlockIncrement/2, ic.Chain.GetConfig().ValidatorsCount))
}
ok, err := checkValidators(ic)
if err != nil {
panic(fmt.Errorf("failed to check committee signature: %w", err))
}
if !ok {
return stackitem.NewBool(false)
}
n.lock.Lock()
defer n.lock.Unlock()
err = setUint32WithKey(n.ContractID, ic.DAO, maxNotValidBeforeDeltaKey, value)
if err != nil {
panic(fmt.Errorf("failed to put value into the storage: %w", err))
}
n.isValid = false
return stackitem.NewBool(true)
}
// GetDepositFor returns state.Deposit for the account specified. It returns nil in case if
// deposit is not found in storage and panics in case of any other error.
func (n *Notary) getDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit {
func (n *Notary) GetDepositFor(dao dao.DAO, acc util.Uint160) *state.Deposit {
key := append([]byte{prefixDeposit}, acc.BytesBE()...)
deposit := new(state.Deposit)
err := getSerializableFromDAO(n.ContractID, dao, key, deposit)

View file

@ -1,7 +1,6 @@
package native
import (
"encoding/binary"
"fmt"
"math/big"
"sort"
@ -10,7 +9,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
@ -167,10 +165,10 @@ func (p *Policy) OnPersistEnd(dao dao.DAO) error {
p.lock.Lock()
defer p.lock.Unlock()
p.maxTransactionsPerBlock = p.getUint32WithKey(dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock)
p.maxBlockSize = p.getUint32WithKey(dao, maxBlockSizeKey, defaultMaxBlockSize)
p.feePerByte = p.getInt64WithKey(dao, feePerByteKey, defaultFeePerByte)
p.maxBlockSystemFee = p.getInt64WithKey(dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee)
p.maxTransactionsPerBlock = getUint32WithKey(p.ContractID, dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock)
p.maxBlockSize = getUint32WithKey(p.ContractID, dao, maxBlockSizeKey, defaultMaxBlockSize)
p.feePerByte = getInt64WithKey(p.ContractID, dao, feePerByteKey, defaultFeePerByte)
p.maxBlockSystemFee = getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee)
p.maxVerificationGas = defaultMaxVerificationGas
p.blockedAccounts = make([]util.Uint160, 0)
@ -207,7 +205,7 @@ func (p *Policy) GetMaxTransactionsPerBlockInternal(dao dao.DAO) uint32 {
if p.isValid {
return p.maxTransactionsPerBlock
}
return p.getUint32WithKey(dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock)
return getUint32WithKey(p.ContractID, dao, maxTransactionsPerBlockKey, defaultMaxTransactionsPerBlock)
}
// getMaxBlockSize is Policy contract method and returns maximum block size.
@ -222,7 +220,7 @@ func (p *Policy) GetMaxBlockSizeInternal(dao dao.DAO) uint32 {
if p.isValid {
return p.maxBlockSize
}
return p.getUint32WithKey(dao, maxBlockSizeKey, defaultMaxBlockSize)
return getUint32WithKey(p.ContractID, dao, maxBlockSizeKey, defaultMaxBlockSize)
}
// getFeePerByte is Policy contract method and returns required transaction's fee
@ -238,7 +236,7 @@ func (p *Policy) GetFeePerByteInternal(dao dao.DAO) int64 {
if p.isValid {
return p.feePerByte
}
return p.getInt64WithKey(dao, feePerByteKey, defaultFeePerByte)
return getInt64WithKey(p.ContractID, dao, feePerByteKey, defaultFeePerByte)
}
// GetMaxVerificationGas returns maximum gas allowed to be burned during verificaion.
@ -262,7 +260,7 @@ func (p *Policy) GetMaxBlockSystemFeeInternal(dao dao.DAO) int64 {
if p.isValid {
return p.maxBlockSystemFee
}
return p.getInt64WithKey(dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee)
return getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee)
}
// isBlocked is Policy contract method and checks whether provided account is blocked.
@ -296,7 +294,7 @@ func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackite
if value > block.MaxTransactionsPerBlock {
panic(fmt.Errorf("MaxTransactionsPerBlock cannot exceed the maximum allowed transactions per block = %d", block.MaxTransactionsPerBlock))
}
ok, err := p.checkValidators(ic)
ok, err := checkValidators(ic)
if err != nil {
panic(err)
}
@ -305,7 +303,7 @@ func (p *Policy) setMaxTransactionsPerBlock(ic *interop.Context, args []stackite
}
p.lock.Lock()
defer p.lock.Unlock()
err = p.setUint32WithKey(ic.DAO, maxTransactionsPerBlockKey, value)
err = setUint32WithKey(p.ContractID, ic.DAO, maxTransactionsPerBlockKey, value)
if err != nil {
panic(err)
}
@ -319,7 +317,7 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta
if value > payload.MaxSize {
panic(fmt.Errorf("MaxBlockSize cannot be more than the maximum payload size = %d", payload.MaxSize))
}
ok, err := p.checkValidators(ic)
ok, err := checkValidators(ic)
if err != nil {
panic(err)
}
@ -328,7 +326,7 @@ func (p *Policy) setMaxBlockSize(ic *interop.Context, args []stackitem.Item) sta
}
p.lock.Lock()
defer p.lock.Unlock()
err = p.setUint32WithKey(ic.DAO, maxBlockSizeKey, value)
err = setUint32WithKey(p.ContractID, ic.DAO, maxBlockSizeKey, value)
if err != nil {
panic(err)
}
@ -342,7 +340,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack
if value < 0 || value > maxFeePerByte {
panic(fmt.Errorf("FeePerByte shouldn't be negative or greater than %d", maxFeePerByte))
}
ok, err := p.checkValidators(ic)
ok, err := checkValidators(ic)
if err != nil {
panic(err)
}
@ -351,7 +349,7 @@ func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stack
}
p.lock.Lock()
defer p.lock.Unlock()
err = p.setInt64WithKey(ic.DAO, feePerByteKey, value)
err = setInt64WithKey(p.ContractID, ic.DAO, feePerByteKey, value)
if err != nil {
panic(err)
}
@ -365,7 +363,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item
if value <= minBlockSystemFee {
panic(fmt.Errorf("MaxBlockSystemFee cannot be less then %d", minBlockSystemFee))
}
ok, err := p.checkValidators(ic)
ok, err := checkValidators(ic)
if err != nil {
panic(err)
}
@ -374,7 +372,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item
}
p.lock.Lock()
defer p.lock.Unlock()
err = p.setInt64WithKey(ic.DAO, maxBlockSystemFeeKey, value)
err = setInt64WithKey(p.ContractID, ic.DAO, maxBlockSystemFeeKey, value)
if err != nil {
panic(err)
}
@ -385,7 +383,7 @@ func (p *Policy) setMaxBlockSystemFee(ic *interop.Context, args []stackitem.Item
// blockAccount is Policy contract method and adds given account hash to the list
// of blocked accounts.
func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
ok, err := p.checkValidators(ic)
ok, err := checkValidators(ic)
if err != nil {
panic(err)
}
@ -412,7 +410,7 @@ func (p *Policy) blockAccount(ic *interop.Context, args []stackitem.Item) stacki
// unblockAccount is Policy contract method and removes given account hash from
// the list of blocked accounts.
func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
ok, err := p.checkValidators(ic)
ok, err := checkValidators(ic)
if err != nil {
panic(err)
}
@ -434,46 +432,6 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
return stackitem.NewBool(true)
}
func (p *Policy) getUint32WithKey(dao dao.DAO, key []byte, defaultValue uint32) uint32 {
si := dao.GetStorageItem(p.ContractID, key)
if si == nil {
return defaultValue
}
return binary.LittleEndian.Uint32(si.Value)
}
func (p *Policy) setUint32WithKey(dao dao.DAO, key []byte, value uint32) error {
si := &state.StorageItem{
Value: make([]byte, 4),
}
binary.LittleEndian.PutUint32(si.Value, value)
return dao.PutStorageItem(p.ContractID, key, si)
}
func (p *Policy) getInt64WithKey(dao dao.DAO, key []byte, defaultValue int64) int64 {
si := dao.GetStorageItem(p.ContractID, key)
if si == nil {
return defaultValue
}
return int64(binary.LittleEndian.Uint64(si.Value))
}
func (p *Policy) setInt64WithKey(dao dao.DAO, key []byte, value int64) error {
si := &state.StorageItem{
Value: make([]byte, 8),
}
binary.LittleEndian.PutUint64(si.Value, uint64(value))
return dao.PutStorageItem(p.ContractID, key, si)
}
func (p *Policy) checkValidators(ic *interop.Context) (bool, error) {
prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash)
if err != nil {
return false, err
}
return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus)
}
// CheckPolicy checks whether transaction conforms to current policy restrictions
// like not being signed by blocked account or not exceeding block-level system
// fee limit.

View file

@ -1,7 +1,11 @@
package native
import (
"encoding/binary"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/io"
@ -27,3 +31,43 @@ func putSerializableToDAO(id int32, d dao.DAO, key []byte, item io.Serializable)
Value: w.Bytes(),
})
}
func getInt64WithKey(id int32, d dao.DAO, key []byte, defaultValue int64) int64 {
si := d.GetStorageItem(id, key)
if si == nil {
return defaultValue
}
return int64(binary.LittleEndian.Uint64(si.Value))
}
func setInt64WithKey(id int32, dao dao.DAO, key []byte, value int64) error {
si := &state.StorageItem{
Value: make([]byte, 8),
}
binary.LittleEndian.PutUint64(si.Value, uint64(value))
return dao.PutStorageItem(id, key, si)
}
func getUint32WithKey(id int32, dao dao.DAO, key []byte, defaultValue uint32) uint32 {
si := dao.GetStorageItem(id, key)
if si == nil {
return defaultValue
}
return binary.LittleEndian.Uint32(si.Value)
}
func setUint32WithKey(id int32, dao dao.DAO, key []byte, value uint32) error {
si := &state.StorageItem{
Value: make([]byte, 4),
}
binary.LittleEndian.PutUint32(si.Value, value)
return dao.PutStorageItem(id, key, si)
}
func checkValidators(ic *interop.Context) (bool, error) {
prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash)
if err != nil {
return false, err
}
return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus)
}

View file

@ -2,12 +2,14 @@ package core
import (
"math"
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -319,3 +321,48 @@ func TestNotaryNodesReward(t *testing.T) {
checkReward(5, 7, spendDeposit)
}
}
func TestMaxNotValidBeforeDelta(t *testing.T) {
chain := newTestChain(t)
defer chain.Close()
notaryHash := chain.contracts.Notary.Hash
t.Run("get, internal method", func(t *testing.T) {
n := chain.contracts.Notary.GetMaxNotValidBeforeDelta(chain.dao)
require.Equal(t, 140, int(n))
})
t.Run("get, contract method", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, notaryHash, "getMaxNotValidBeforeDelta")
require.NoError(t, err)
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(140)))
require.NoError(t, chain.persist())
})
t.Run("set", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(150)))
require.NoError(t, err)
checkResult(t, res, stackitem.NewBool(true))
n := chain.contracts.Notary.GetMaxNotValidBeforeDelta(chain.dao)
require.Equal(t, 150, int(n))
})
t.Run("set, too big value", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(transaction.MaxValidUntilBlockIncrement/2+1)))
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("set, too small value", func(t *testing.T) {
res, err := invokeContractMethod(chain, 100000000, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(int64(chain.GetConfig().ValidatorsCount-1))))
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("set, not signed by committee", func(t *testing.T) {
signer, err := wallet.NewAccount()
require.NoError(t, err)
invokeRes, err := invokeContractMethodBy(t, chain, signer, notaryHash, "setMaxNotValidBeforeDelta", bigint.ToBytes(big.NewInt(150)))
checkResult(t, invokeRes, stackitem.NewBool(false))
})
}

View file

@ -11,6 +11,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
@ -45,6 +46,13 @@ func TestMaxTransactionsPerBlock(t *testing.T) {
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("set, not signed by committee", func(t *testing.T) {
signer, err := wallet.NewAccount()
require.NoError(t, err)
invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setMaxTransactionsPerBlock", bigint.ToBytes(big.NewInt(1024)))
checkResult(t, invokeRes, stackitem.NewBool(false))
})
}
func TestMaxBlockSize(t *testing.T) {
@ -80,6 +88,13 @@ func TestMaxBlockSize(t *testing.T) {
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("set, not signed by committee", func(t *testing.T) {
signer, err := wallet.NewAccount()
require.NoError(t, err)
invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setMaxBlockSize", bigint.ToBytes(big.NewInt(102400)))
checkResult(t, invokeRes, stackitem.NewBool(false))
})
}
func TestFeePerByte(t *testing.T) {
@ -119,6 +134,13 @@ func TestFeePerByte(t *testing.T) {
require.NoError(t, err)
checkFAULTState(t, res)
})
t.Run("set, not signed by committee", func(t *testing.T) {
signer, err := wallet.NewAccount()
require.NoError(t, err)
invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setFeePerByte", bigint.ToBytes(big.NewInt(1024)))
checkResult(t, invokeRes, stackitem.NewBool(false))
})
}
func TestBlockSystemFee(t *testing.T) {
@ -154,6 +176,13 @@ func TestBlockSystemFee(t *testing.T) {
checkResult(t, res, stackitem.NewBigInteger(big.NewInt(100000000)))
require.NoError(t, chain.persist())
})
t.Run("set, not signed by committee", func(t *testing.T) {
signer, err := wallet.NewAccount()
require.NoError(t, err)
invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "setMaxBlockSystemFee", bigint.ToBytes(big.NewInt(100000000)))
checkResult(t, invokeRes, stackitem.NewBool(false))
})
}
func TestBlockedAccounts(t *testing.T) {
@ -217,4 +246,11 @@ func TestBlockedAccounts(t *testing.T) {
checkResult(t, res, stackitem.NewBool(false))
require.NoError(t, chain.persist())
})
t.Run("not signed by committee", func(t *testing.T) {
signer, err := wallet.NewAccount()
require.NoError(t, err)
invokeRes, err := invokeContractMethodBy(t, chain, signer, policyHash, "blockAccount", account.BytesBE())
checkResult(t, invokeRes, stackitem.NewBool(false))
})
}

View file

@ -13,6 +13,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -34,18 +35,27 @@ type testChain struct {
blocksCh []chan<- *block.Block
blockheight uint32
poolTx func(*transaction.Transaction) error
poolTxWithData func(*transaction.Transaction, interface{}, *mempool.Pool) error
blocks map[util.Uint256]*block.Block
hdrHashes map[uint32]util.Uint256
txs map[util.Uint256]*transaction.Transaction
verifyWitnessF func() error
maxVerificationGAS int64
notaryContractScriptHash util.Uint160
notaryDepositExpiration uint32
postBlock []func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)
utilityTokenBalance *big.Int
}
func newTestChain() *testChain {
return &testChain{
Pool: mempool.New(10),
Pool: mempool.New(10, 0),
poolTx: func(*transaction.Transaction) error { return nil },
poolTxWithData: func(*transaction.Transaction, interface{}, *mempool.Pool) error { return nil },
blocks: make(map[util.Uint256]*block.Block),
hdrHashes: make(map[uint32]util.Uint256),
txs: make(map[util.Uint256]*transaction.Transaction),
ProtocolConfiguration: config.ProtocolConfiguration{P2PNotaryRequestPayloadPoolSize: 10},
}
}
@ -65,6 +75,48 @@ func (chain *testChain) putTx(tx *transaction.Transaction) {
func (chain *testChain) ApplyPolicyToTxSet([]*transaction.Transaction) []*transaction.Transaction {
panic("TODO")
}
func (chain *testChain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool {
panic("TODO")
}
func (chain *testChain) GetNotaryDepositExpiration(acc util.Uint160) uint32 {
if chain.notaryDepositExpiration != 0 {
return chain.notaryDepositExpiration
}
panic("TODO")
}
func (chain *testChain) GetNotaryContractScriptHash() util.Uint160 {
if !chain.notaryContractScriptHash.Equals(util.Uint160{}) {
return chain.notaryContractScriptHash
}
panic("TODO")
}
func (chain *testChain) GetNotaryBalance(acc util.Uint160) *big.Int {
panic("TODO")
}
func (chain *testChain) GetPolicer() blockchainer.Policer {
return chain
}
func (chain *testChain) GetMaxVerificationGAS() int64 {
if chain.maxVerificationGAS != 0 {
return chain.maxVerificationGAS
}
panic("TODO")
}
func (chain *testChain) PoolTxWithData(t *transaction.Transaction, data interface{}, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(bc blockchainer.Blockchainer, t *transaction.Transaction, data interface{}) error) error {
return chain.poolTxWithData(t, data, mp)
}
func (chain *testChain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) {
chain.postBlock = append(chain.postBlock, f)
}
func (chain *testChain) GetConfig() config.ProtocolConfiguration {
return chain.ProtocolConfiguration
}
@ -77,7 +129,7 @@ func (chain *testChain) FeePerByte() int64 {
}
func (chain *testChain) P2PSigExtensionsEnabled() bool {
return false
return true
}
func (chain *testChain) GetMaxBlockSystemFee() int64 {
@ -207,6 +259,9 @@ func (chain *testChain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, ui
}
func (chain *testChain) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
if chain.utilityTokenBalance != nil {
return chain.utilityTokenBalance
}
panic("TODO")
}
@ -230,7 +285,10 @@ func (chain *testChain) SubscribeForTransactions(ch chan<- *transaction.Transact
func (chain *testChain) VerifyTx(*transaction.Transaction) error {
panic("TODO")
}
func (*testChain) VerifyWitness(util.Uint160, crypto.Verifiable, *transaction.Witness, int64) error {
func (chain *testChain) VerifyWitness(util.Uint160, crypto.Verifiable, *transaction.Witness, int64) error {
if chain.verifyWitnessF != nil {
return chain.verifyWitnessF()
}
panic("TODO")
}

View file

@ -75,6 +75,7 @@ const (
CMDTX = CommandType(payload.TXType)
CMDBlock = CommandType(payload.BlockType)
CMDConsensus = CommandType(payload.ConsensusType)
CMDP2PNotaryRequest = CommandType(payload.P2PNotaryRequestType)
CMDReject CommandType = 0x2f
// SPV protocol
@ -148,6 +149,8 @@ func (m *Message) decodePayload() error {
p = block.New(m.Network, m.StateRootInHeader)
case CMDConsensus:
p = consensus.NewPayload(m.Network, m.StateRootInHeader)
case CMDP2PNotaryRequest:
p = &payload.P2PNotaryRequest{Network: m.Network}
case CMDGetBlocks:
p = &payload.GetBlocks{}
case CMDGetHeaders:

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

View file

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

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/block"
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
"github.com/nspcc-dev/neo-go/pkg/core/mempool"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/network/capability"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
@ -64,6 +65,8 @@ type (
chain blockchainer.Blockchainer
bQueue *blockQueue
consensus consensus.Service
notaryRequestPool *mempool.Pool
NotaryFeer NotaryFeer
lock sync.RWMutex
peers map[Peer]bool
@ -124,6 +127,15 @@ func newServerFromConstructors(config ServerConfig, chain blockchainer.Blockchai
log: log,
transactions: make(chan *transaction.Transaction, 64),
}
if chain.P2PSigExtensionsEnabled() {
s.NotaryFeer = NewNotaryFeer(chain)
s.notaryRequestPool = mempool.New(chain.GetConfig().P2PNotaryRequestPayloadPoolSize, 1)
chain.RegisterPostBlock(func(bc blockchainer.Blockchainer, txpool *mempool.Pool, _ *block.Block) {
s.notaryRequestPool.RemoveStale(func(t *transaction.Transaction) bool {
return bc.IsTxStillRelevant(t, txpool, true)
}, s.NotaryFeer)
})
}
s.bQueue = newBlockQueue(maxBlockBatch, chain, log, func(b *block.Block) {
if !s.consensusStarted.Load() {
s.tryStartConsensus()
@ -188,7 +200,7 @@ func (s *Server) Start(errChan chan error) {
zap.Uint32("headerHeight", s.chain.HeaderHeight()))
s.tryStartConsensus()
s.initStaleTxMemPool()
s.initStaleMemPools()
go s.broadcastTxLoop()
go s.relayBlocksLoop()
@ -507,6 +519,9 @@ func (s *Server) handleInvCmd(p Peer, inv *payload.Inventory) error {
cp := s.consensus.GetPayload(h)
return cp != nil
},
payload.P2PNotaryRequestType: func(h util.Uint256) bool {
return s.notaryRequestPool.ContainsKey(h)
},
}
if exists := typExists[inv.Type]; exists != nil {
for _, hash := range inv.Hashes {
@ -573,6 +588,12 @@ func (s *Server) handleGetDataCmd(p Peer, inv *payload.Inventory) error {
if cp := s.consensus.GetPayload(hash); cp != nil {
msg = NewMessage(CMDConsensus, cp)
}
case payload.P2PNotaryRequestType:
if nrp, ok := s.notaryRequestPool.TryGetData(hash); ok { // already have checked P2PSigExtEnabled
msg = NewMessage(CMDP2PNotaryRequest, nrp.(*payload.P2PNotaryRequest))
} else {
notFound = append(notFound, hash)
}
}
if msg != nil {
pkt, err := msg.Bytes()
@ -687,11 +708,62 @@ func (s *Server) handleTxCmd(tx *transaction.Transaction) error {
// in the pool.
if s.verifyAndPoolTX(tx) == RelaySucceed {
s.consensus.OnTransaction(tx)
s.broadcastTX(tx)
s.broadcastTX(tx, nil)
}
return nil
}
// handleP2PNotaryRequestCmd process received P2PNotaryRequest payload.
func (s *Server) handleP2PNotaryRequestCmd(r *payload.P2PNotaryRequest) error {
if !s.chain.P2PSigExtensionsEnabled() {
return errors.New("P2PNotaryRequestCMD was received, but P2PSignatureExtensions are disabled")
}
if s.verifyAndPoolNotaryRequest(r) == RelaySucceed {
s.broadcastP2PNotaryRequestPayload(nil, r)
}
return nil
}
// verifyAndPoolNotaryRequest verifies NotaryRequest payload and adds it to the payload mempool.
func (s *Server) verifyAndPoolNotaryRequest(r *payload.P2PNotaryRequest) RelayReason {
if err := s.chain.PoolTxWithData(r.FallbackTransaction, r, s.notaryRequestPool, s.NotaryFeer, verifyNotaryRequest); err != nil {
switch {
case errors.Is(err, core.ErrAlreadyExists):
return RelayAlreadyExists
case errors.Is(err, core.ErrOOM):
return RelayOutOfMemory
case errors.Is(err, core.ErrPolicy):
return RelayPolicyFail
default:
return RelayInvalid
}
}
return RelaySucceed
}
// verifyNotaryRequest is a function for state-dependant P2PNotaryRequest payload verification which is executed before ordinary blockchain's verification.
func verifyNotaryRequest(bc blockchainer.Blockchainer, _ *transaction.Transaction, data interface{}) error {
r := data.(*payload.P2PNotaryRequest)
payer := r.FallbackTransaction.Signers[1].Account
if err := bc.VerifyWitness(payer, r, &r.Witness, bc.GetPolicer().GetMaxVerificationGAS()); err != nil {
return fmt.Errorf("bad P2PNotaryRequest payload witness: %w", err)
}
if r.FallbackTransaction.Sender() != bc.GetNotaryContractScriptHash() {
return errors.New("P2PNotary contract should be a sender of the fallback transaction")
}
depositExpiration := bc.GetNotaryDepositExpiration(payer)
if r.FallbackTransaction.ValidUntilBlock >= depositExpiration {
return fmt.Errorf("fallback transaction is valid after deposit is unlocked: ValidUntilBlock is %d, deposit lock expires at %d", r.FallbackTransaction.ValidUntilBlock, depositExpiration)
}
return nil
}
func (s *Server) broadcastP2PNotaryRequestPayload(_ *transaction.Transaction, data interface{}) {
r := data.(payload.P2PNotaryRequest) // we can guarantee that cast is successful
msg := NewMessage(CMDInv, payload.NewInventory(payload.P2PNotaryRequestType, []util.Uint256{r.FallbackTransaction.Hash()}))
s.broadcastMessage(msg)
}
// handleAddrCmd will process received addresses.
func (s *Server) handleAddrCmd(p Peer, addrs *payload.AddressList) error {
if !p.CanProcessAddr() {
@ -770,7 +842,7 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error {
if peer.Handshaked() {
if inv, ok := msg.Payload.(*payload.Inventory); ok {
if !inv.Type.Valid() || len(inv.Hashes) == 0 {
if !inv.Type.Valid(s.chain.P2PSigExtensionsEnabled()) || len(inv.Hashes) == 0 {
return errInvalidInvType
}
}
@ -808,6 +880,9 @@ func (s *Server) handleMessage(peer Peer, msg *Message) error {
case CMDTX:
tx := msg.Payload.(*transaction.Transaction)
return s.handleTxCmd(tx)
case CMDP2PNotaryRequest:
r := msg.Payload.(*payload.P2PNotaryRequest)
return s.handleP2PNotaryRequestCmd(r)
case CMDPing:
ping := msg.Payload.(*payload.Ping)
return s.handlePing(peer, ping)
@ -943,13 +1018,13 @@ func (s *Server) verifyAndPoolTX(t *transaction.Transaction) RelayReason {
func (s *Server) RelayTxn(t *transaction.Transaction) RelayReason {
ret := s.verifyAndPoolTX(t)
if ret == RelaySucceed {
s.broadcastTX(t)
s.broadcastTX(t, nil)
}
return ret
}
// broadcastTX broadcasts an inventory message about new transaction.
func (s *Server) broadcastTX(t *transaction.Transaction) {
func (s *Server) broadcastTX(t *transaction.Transaction, _ interface{}) {
select {
case s.transactions <- t:
case <-s.quit:
@ -964,8 +1039,8 @@ func (s *Server) broadcastTxHashes(hs []util.Uint256) {
s.iteratePeersWithSendMsg(msg, Peer.EnqueuePacket, Peer.IsFullNode)
}
// initStaleTxMemPool initializes mempool for stale tx processing.
func (s *Server) initStaleTxMemPool() {
// initStaleMemPools initializes mempools for stale tx/payload processing.
func (s *Server) initStaleMemPools() {
cfg := s.chain.GetConfig()
threshold := 5
if cfg.ValidatorsCount*2 > threshold {
@ -974,6 +1049,9 @@ func (s *Server) initStaleTxMemPool() {
mp := s.chain.GetMemPool()
mp.SetResendThreshold(uint32(threshold), s.broadcastTX)
if s.chain.P2PSigExtensionsEnabled() {
s.notaryRequestPool.SetResendThreshold(uint32(threshold), s.broadcastP2PNotaryRequestPayload)
}
}
// broadcastTxLoop is a loop for batching and sending

View file

@ -19,6 +19,8 @@ import (
"github.com/nspcc-dev/neo-go/pkg/network/capability"
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
@ -445,7 +447,7 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType,
p.handshaked = true
p.messageHandler = func(t *testing.T, msg *Message) {
switch msg.Command {
case CMDTX, CMDBlock, CMDConsensus:
case CMDTX, CMDBlock, CMDConsensus, CMDP2PNotaryRequest:
require.Equal(t, found, msg.Payload)
recvResponse.Store(true)
case CMDNotFound:
@ -463,6 +465,7 @@ func (s *Server) testHandleGetData(t *testing.T, invType payload.InventoryType,
func TestGetData(t *testing.T) {
s, shutdown := startTestServer(t)
defer shutdown()
s.chain.(*testChain).utilityTokenBalance = big.NewInt(1000000)
t.Run("block", func(t *testing.T) {
b := newDummyBlock(2, 0)
@ -478,6 +481,44 @@ func TestGetData(t *testing.T) {
notFound := []util.Uint256{hs[0], hs[2]}
s.testHandleGetData(t, payload.TXType, hs, notFound, tx)
})
t.Run("p2pNotaryRequest", func(t *testing.T) {
mainTx := &transaction.Transaction{
Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
Script: []byte{0, 1, 2},
ValidUntilBlock: 123,
Signers: []transaction.Signer{{Account: random.Uint160()}},
Scripts: []transaction.Witness{{InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}},
}
mainTx.Size()
mainTx.Hash()
fallbackTx := &transaction.Transaction{
Script: []byte{1, 2, 3},
ValidUntilBlock: 123,
Attributes: []transaction.Attribute{
{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
},
Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 64}, make([]byte, 64, 64)...), VerificationScript: make([]byte, 0)}, {InvocationScript: []byte{}, VerificationScript: []byte{}}},
}
fallbackTx.Size()
fallbackTx.Hash()
r := &payload.P2PNotaryRequest{
MainTransaction: mainTx,
FallbackTransaction: fallbackTx,
Network: netmode.UnitTestNet,
Witness: transaction.Witness{
InvocationScript: []byte{1, 2, 3},
VerificationScript: []byte{1, 2, 3},
},
}
r.Hash()
require.NoError(t, s.notaryRequestPool.Add(r.FallbackTransaction, s.chain, r))
hs := []util.Uint256{random.Uint256(), r.FallbackTransaction.Hash(), random.Uint256()}
notFound := []util.Uint256{hs[0], hs[2]}
s.testHandleGetData(t, payload.P2PNotaryRequestType, hs, notFound, r)
})
}
func initGetBlocksTest(t *testing.T) (*Server, func(), []*block.Block) {
@ -602,6 +643,7 @@ func TestGetHeaders(t *testing.T) {
func TestInv(t *testing.T) {
s, shutdown := startTestServer(t)
defer shutdown()
s.chain.(*testChain).utilityTokenBalance = big.NewInt(10000000)
var actual []util.Uint256
p := newLocalPeer(t, s)
@ -632,6 +674,24 @@ func TestInv(t *testing.T) {
})
require.Equal(t, []util.Uint256{hs[0], hs[2]}, actual)
})
t.Run("p2pNotaryRequest", func(t *testing.T) {
fallbackTx := transaction.New(netmode.UnitTestNet, random.Bytes(100), 123)
fallbackTx.Signers = []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}
fallbackTx.Size()
fallbackTx.Hash()
r := &payload.P2PNotaryRequest{
MainTransaction: newDummyTx(),
FallbackTransaction: fallbackTx,
Network: netmode.UnitTestNet,
}
require.NoError(t, s.notaryRequestPool.Add(r.FallbackTransaction, s.chain, r))
hs := []util.Uint256{random.Uint256(), r.FallbackTransaction.Hash(), random.Uint256()}
s.testHandleMessage(t, p, CMDInv, &payload.Inventory{
Type: payload.P2PNotaryRequestType,
Hashes: hs,
})
require.Equal(t, []util.Uint256{hs[0], hs[2]}, actual)
})
}
func TestRequestTx(t *testing.T) {
@ -763,3 +823,43 @@ func TestMemPool(t *testing.T) {
s.testHandleMessage(t, p, CMDMempool, payload.NullPayload{})
require.ElementsMatch(t, expected, actual)
}
func TestVerifyNotaryRequest(t *testing.T) {
bc := newTestChain()
bc.maxVerificationGAS = 10
bc.notaryContractScriptHash = util.Uint160{1, 2, 3}
newNotaryRequest := func() *payload.P2PNotaryRequest {
return &payload.P2PNotaryRequest{
MainTransaction: &transaction.Transaction{Script: []byte{0, 1, 2}},
FallbackTransaction: &transaction.Transaction{
ValidUntilBlock: 321,
Signers: []transaction.Signer{{Account: bc.notaryContractScriptHash}, {Account: random.Uint160()}},
},
Witness: transaction.Witness{},
}
}
t.Run("bad payload witness", func(t *testing.T) {
bc.verifyWitnessF = func() error { return errors.New("bad witness") }
require.Error(t, verifyNotaryRequest(bc, nil, newNotaryRequest()))
})
t.Run("bad fallback sender", func(t *testing.T) {
bc.verifyWitnessF = func() error { return nil }
r := newNotaryRequest()
r.FallbackTransaction.Signers[0] = transaction.Signer{Account: util.Uint160{7, 8, 9}}
require.Error(t, verifyNotaryRequest(bc, nil, r))
})
t.Run("expired deposit", func(t *testing.T) {
r := newNotaryRequest()
bc.notaryDepositExpiration = r.FallbackTransaction.ValidUntilBlock
require.Error(t, verifyNotaryRequest(bc, nil, r))
})
t.Run("good", func(t *testing.T) {
r := newNotaryRequest()
bc.notaryDepositExpiration = r.FallbackTransaction.ValidUntilBlock + 1
require.NoError(t, verifyNotaryRequest(bc, nil, r))
})
}