diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 1ac2f1c809..b1860b8c4b 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -9,6 +9,9 @@ type ( ProtocolConfiguration struct { Magic netmode.Magic `yaml:"Magic"` MemPoolSize int `yaml:"MemPoolSize"` + // P2PNotaryRequestPayloadPoolSize specifies the memory pool size for P2PNotaryRequestPayloads. + // It is valid only if P2PSigExtensions are enabled. + P2PNotaryRequestPayloadPoolSize int `yaml:"P2PNotaryRequestPayloadPoolSize"` // KeepOnlyLatestState specifies if MPT should only store latest state. // If true, DB size will be smaller, but older roots won't be accessible. // This value should remain the same for the same database. @@ -17,7 +20,7 @@ type ( RemoveUntraceableBlocks bool `yaml:"RemoveUntraceableBlocks"` // MaxTraceableBlocks is the length of the chain accessible to smart contracts. MaxTraceableBlocks uint32 `yaml:"MaxTraceableBlocks"` - // P2PSigExtensions enables additional signature-related transaction attributes + // P2PSigExtensions enables additional signature-related logic. P2PSigExtensions bool `yaml:"P2PSigExtensions"` // ReservedAttributes allows to have reserved attributes range for experimental or private purposes. ReservedAttributes bool `yaml:"ReservedAttributes"` diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index b07945fe7a..dc8cbabe4e 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/block" + "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/mempool" @@ -26,6 +27,7 @@ import ( "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/network/payload" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" @@ -42,9 +44,10 @@ const ( headerBatchCount = 2000 version = "0.1.0" - defaultMemPoolSize = 50000 - defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks - verificationGasLimit = 100000000 // 1 GAS + defaultMemPoolSize = 50000 + defaultP2PNotaryRequestPayloadPoolSize = 1000 + defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks + verificationGasLimit = 100000000 // 1 GAS ) var ( @@ -116,6 +119,10 @@ type Blockchain struct { memPool *mempool.Pool + // Callback methods for NotaryRequestPool which should be run under the blockchain lock. + poolP2PNotaryRequestCallback func(blockchainer.Blockchainer, *payload.P2PNotaryRequest) error + postBlock func(blockchainer.Blockchainer, *mempool.Pool, *block.Block) + sbCommittee keys.PublicKeys log *zap.Logger @@ -151,6 +158,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)) @@ -720,6 +731,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 @@ -734,6 +752,9 @@ 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) + if bc.postBlock != nil { + bc.postBlock(bc, txpool, block) + } bc.lock.Unlock() updateBlockHeightMetric(block.Index) @@ -934,6 +955,11 @@ func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint return &neo.Balance, neo.LastUpdatedBlock } +// GetDepositFor returns GAS amount deposited to Notary contract for the specified account. +func (bc *Blockchain) GetDepositFor(acc util.Uint160) *big.Int { + return bc.contracts.Notary.BalanceOf(bc.dao, acc) +} + // LastBatch returns last persisted storage batch. func (bc *Blockchain) LastBatch() *storage.MemBatch { return bc.lastBatch @@ -1548,6 +1574,14 @@ func (bc *Blockchain) PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) return bc.verifyAndPoolTx(t, pool) } +// PoolP2PNotaryRequestPayload verifies NotaryRequest payload and adds it to the memory pool. +func (bc *Blockchain) PoolP2PNotaryRequestPayload(r *payload.P2PNotaryRequest) error { + bc.lock.RLock() + defer bc.lock.RUnlock() + + return bc.poolP2PNotaryRequestCallback(bc, r) +} + //GetStandByValidators returns validators from the configuration. func (bc *Blockchain) GetStandByValidators() keys.PublicKeys { return bc.sbCommittee[:bc.config.ValidatorsCount].Copy() @@ -1757,3 +1791,34 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d dao.DAO, block * func (bc *Blockchain) P2PSigExtensionsEnabled() bool { return bc.config.P2PSigExtensions } + +func (bc *Blockchain) GetMaxVerificationGas() int64 { + return bc.contracts.Policy.GetMaxVerificationGas(bc.dao) +} + +func (bc *Blockchain) NotaryContractHash() util.Uint160 { + if bc.P2PSigExtensionsEnabled() { + return bc.contracts.Notary.Hash + } + return util.Uint160{} +} + +func (bc *Blockchain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) { + if bc.P2PSigExtensionsEnabled() { + bc.postBlock = f + } +} + +func (bc *Blockchain) RegisterPoolNotaryRequestCallback(f func(blockchainer.Blockchainer, *payload.P2PNotaryRequest) error) { + if bc.P2PSigExtensionsEnabled() { + bc.poolP2PNotaryRequestCallback = f + } +} + +func (bc *Blockchain) GetMaxNotValidBeforeDelta() int64 { + return bc.contracts.Notary.GetMaxNotValidBeforeDelta(bc.dao) +} + +func (bc *Blockchain) ExpirationOf(acc util.Uint160) uint32 { + return bc.contracts.Notary.ExpirationOf(bc.dao, acc) +} diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 422e1ff468..9691fc0fc4 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -67,4 +68,14 @@ type Blockchainer interface { UnsubscribeFromExecutions(ch chan<- *state.AppExecResult) UnsubscribeFromNotifications(ch chan<- *state.NotificationEvent) UnsubscribeFromTransactions(ch chan<- *transaction.Transaction) + + // signature extensions + RegisterPostBlock(f func(Blockchainer, *mempool.Pool, *block.Block)) + RegisterPoolNotaryRequestCallback(f func(Blockchainer, *payload.P2PNotaryRequest) error) + PoolP2PNotaryRequestPayload(r *payload.P2PNotaryRequest) error + NotaryContractHash() util.Uint160 + GetMaxVerificationGas() int64 + GetMaxNotValidBeforeDelta() int64 + ExpirationOf(acc util.Uint160) uint32 + GetDepositFor(acc util.Uint160) *big.Int } diff --git a/pkg/core/native/notary.go b/pkg/core/native/notary.go index 9282c52113..99d6bd3218 100644 --- a/pkg/core/native/notary.go +++ b/pkg/core/native/notary.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "sync" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -26,6 +27,13 @@ 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 int64 } const ( @@ -33,9 +41,12 @@ const ( notaryContractID = reservedContractID - 1 // prefixDeposit is a prefix for storing Notary deposits. - prefixDeposit = 1 + prefixDeposit = 1 + 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)} @@ -75,6 +86,14 @@ func newNotary() *Notary { md = newMethodAndPrice(n.verify, 100_0000, smartcontract.AllowStates) n.AddMethod(md, desc, false) + desc = newDescriptor("getMaxNotValidBeforeDelta", smartcontract.IntegerType) + md = newMethodAndPrice(n.getMaxNotValidBeforeDelta, 100_0000, smartcontract.AllowStates) + n.AddMethod(md, desc, false) + + desc = newDescriptor("setMaxNotValidBeforeDelta", smartcontract.BoolType) + md = newMethodAndPrice(n.setMaxNotValidBeforeDelta, 300_0000, smartcontract.AllowModifyStates) + n.AddMethod(md, desc, false) + desc = newDescriptor("onPersist", smartcontract.VoidType) md = newMethodAndPrice(getOnPersistWrapper(n.OnPersist), 0, smartcontract.AllowModifyStates) n.AddMethod(md, desc, false) @@ -93,6 +112,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 } @@ -141,6 +162,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 = getInt64WithKey(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 { @@ -259,21 +293,31 @@ func (n *Notary) withdraw(ic *interop.Context, args []stackitem.Item) stackitem. // balanceOf returns deposited GAS amount for specified address. func (n *Notary) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { acc := toUint160(args[0]) - deposit := n.getDepositFor(ic.DAO, acc) + return stackitem.NewBigInteger(n.BalanceOf(ic.DAO, acc)) +} + +// BalanceOf is an internal representation of `balanceOf` Notary method. +func (n *Notary) BalanceOf(dao dao.DAO, acc util.Uint160) *big.Int { + deposit := n.getDepositFor(dao, acc) if deposit == nil { - return stackitem.NewBigInteger(big.NewInt(0)) + return big.NewInt(0) } - return stackitem.NewBigInteger(deposit.Amount) + return deposit.Amount } // expirationOf Returns deposit lock height for specified address. func (n *Notary) expirationOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { acc := toUint160(args[0]) - deposit := n.getDepositFor(ic.DAO, acc) + return stackitem.Make(n.ExpirationOf(ic.DAO, acc)) +} + +// ExpirationOf is an internal representation of `expirationOf` Notary method. +func (n *Notary) ExpirationOf(dao dao.DAO, acc util.Uint160) uint32 { + deposit := n.getDepositFor(dao, acc) if deposit == nil { - return stackitem.Make(0) + return 0 } - return stackitem.Make(deposit.Till) + return deposit.Till } // verify checks whether the transaction was signed by one of the notaries. @@ -324,6 +368,44 @@ func (n *Notary) GetNotaryNodes(d dao.DAO) (keys.PublicKeys, error) { return nodes, err } +// 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(n.GetMaxNotValidBeforeDelta(ic.DAO))) +} + +// GetMaxNotValidBeforeDelta is an internal representation of Notary getMaxNotValidBeforeDelta method. +func (n *Notary) GetMaxNotValidBeforeDelta(dao dao.DAO) int64 { + n.lock.RLock() + defer n.lock.RUnlock() + if n.isValid { + return n.maxNotValidBeforeDelta + } + return getInt64WithKey(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 := toBigInt(args[0]).Int64() + if value < defaultMaxNotValidBeforeDelta { // TODO: another rule? + panic(fmt.Errorf("MaxNotValidBeforeDelta cannot be less than the default value = %d", defaultMaxNotValidBeforeDelta)) + } + ok, err := checkValidators(ic) + if err != nil { + panic(err) + } + if !ok { + return stackitem.NewBool(false) + } + n.lock.Lock() + defer n.lock.Unlock() + err = setInt64WithKey(n.ContractID, ic.DAO, maxNotValidBeforeDeltaKey, value) + if err != nil { + panic(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 { diff --git a/pkg/core/native/policy.go b/pkg/core/native/policy.go index bcab99498c..53efdf28c2 100644 --- a/pkg/core/native/policy.go +++ b/pkg/core/native/policy.go @@ -10,7 +10,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" @@ -169,8 +168,8 @@ func (p *Policy) OnPersistEnd(dao dao.DAO) error { 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.feePerByte = getInt64WithKey(p.ContractID, dao, feePerByteKey, defaultFeePerByte) + p.maxBlockSystemFee = getInt64WithKey(p.ContractID, dao, maxBlockSystemFeeKey, defaultMaxBlockSystemFee) p.maxVerificationGas = defaultMaxVerificationGas p.blockedAccounts = make([]util.Uint160, 0) @@ -238,7 +237,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 +261,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 +295,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) } @@ -319,7 +318,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) } @@ -342,7 +341,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 +350,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 +364,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 +373,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 +384,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 +411,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) } @@ -450,30 +449,6 @@ func (p *Policy) setUint32WithKey(dao dao.DAO, key []byte, value uint32) error { return dao.PutStorageItem(p.ContractID, key, si) } -func (p *Policy) getInt64WithKey(dao dao.DAO, key []byte, defaultValue int64) int64 { - si := dao.GetStorageItem(p.ContractID, key) - if si == nil { - return defaultValue - } - return int64(binary.LittleEndian.Uint64(si.Value)) -} - -func (p *Policy) setInt64WithKey(dao dao.DAO, key []byte, value int64) error { - si := &state.StorageItem{ - Value: make([]byte, 8), - } - binary.LittleEndian.PutUint64(si.Value, uint64(value)) - return dao.PutStorageItem(p.ContractID, key, si) -} - -func (p *Policy) checkValidators(ic *interop.Context) (bool, error) { - prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash) - if err != nil { - return false, err - } - return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus) -} - // CheckPolicy checks whether transaction conforms to current policy restrictions // like not being signed by blocked account or not exceeding block-level system // fee limit. diff --git a/pkg/core/native/util.go b/pkg/core/native/util.go index 2ca752105e..b879c33bca 100644 --- a/pkg/core/native/util.go +++ b/pkg/core/native/util.go @@ -1,7 +1,11 @@ package native import ( + "encoding/binary" + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/io" @@ -27,3 +31,27 @@ 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 checkValidators(ic *interop.Context) (bool, error) { + prevBlock, err := ic.Chain.GetBlock(ic.Block.PrevHash) + if err != nil { + return false, err + } + return runtime.CheckHashedWitness(ic, prevBlock.NextConsensus) +} diff --git a/pkg/core/native_notary_test.go b/pkg/core/native_notary_test.go index 33d9703a63..708b69eb1e 100644 --- a/pkg/core/native_notary_test.go +++ b/pkg/core/native_notary_test.go @@ -296,3 +296,13 @@ func TestNotaryNodesReward(t *testing.T) { checkReward(5, 7, spendDeposit) } } + +func TestMaxNotValidBeforeDelta(t *testing.T) { + chain := newTestChain(t) + defer chain.Close() + + t.Run("get, internal method", func(t *testing.T) { + n := chain.contracts.Notary.GetMaxNotValidBeforeDelta(chain.dao) + require.Equal(t, 140, int(n)) + }) +} diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 33aa4f2a62..d8814a142a 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -10,6 +10,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/mempool" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -72,6 +73,27 @@ func (chain *testChain) BlockHeight() uint32 { func (chain *testChain) Close() { panic("TODO") } +func (chain *testChain) ExpirationOf(acc util.Uint160) uint32 { + panic("TODO") +} +func (chain *testChain) GetDepositFor(acc util.Uint160) *big.Int { + panic("TODO") +} +func (chain *testChain) GetMaxNotValidBeforeDelta() int64 { + panic("TODO") +} +func (chain *testChain) GetMaxVerificationGas() int64 { + panic("TODO") +} +func (chain *testChain) NotaryContractHash() util.Uint160 { + panic("TODO") +} +func (chain *testChain) RegisterPostBlock(f func(blockchainer.Blockchainer, *mempool.Pool, *block.Block)) { + panic("TODO") +} +func (chain *testChain) RegisterPoolNotaryRequestCallback(f func(blockchainer.Blockchainer, *payload.P2PNotaryRequest) error) { + panic("TODO") +} func (chain testChain) HeaderHeight() uint32 { return 0 } @@ -90,6 +112,9 @@ func (chain testChain) GetContractState(hash util.Uint160) *state.Contract { func (chain testChain) GetContractScriptHash(id int32) (util.Uint160, error) { panic("TODO") } +func (chain testChain) PoolP2PNotaryRequestPayload(r *payload.P2PNotaryRequest) error { + panic("TODO") +} func (chain testChain) GetNativeContractScriptHash(name string) (util.Uint160, error) { panic("TODO") } diff --git a/pkg/network/message.go b/pkg/network/message.go index efa58ad29b..21fd4ea20c 100644 --- a/pkg/network/message.go +++ b/pkg/network/message.go @@ -64,18 +64,19 @@ const ( CMDPong CommandType = 0x19 // synchronization - CMDGetHeaders CommandType = 0x20 - CMDHeaders CommandType = 0x21 - CMDGetBlocks CommandType = 0x24 - CMDMempool CommandType = 0x25 - CMDInv CommandType = 0x27 - CMDGetData CommandType = 0x28 - CMDGetBlockByIndex CommandType = 0x29 - CMDNotFound CommandType = 0x2a - CMDTX = CommandType(payload.TXType) - CMDBlock = CommandType(payload.BlockType) - CMDConsensus = CommandType(payload.ConsensusType) - CMDReject CommandType = 0x2f + CMDGetHeaders CommandType = 0x20 + CMDHeaders CommandType = 0x21 + CMDGetBlocks CommandType = 0x24 + CMDMempool CommandType = 0x25 + CMDInv CommandType = 0x27 + CMDGetData CommandType = 0x28 + CMDGetBlockByIndex CommandType = 0x29 + CMDNotFound CommandType = 0x2a + CMDTX = CommandType(payload.TXType) + CMDBlock = CommandType(payload.BlockType) + CMDConsensus = CommandType(payload.ConsensusType) + CMDP2PNotaryRequest = CommandType(payload.P2PNotaryRequestType) + CMDReject CommandType = 0x2f // SPV protocol CMDFilterLoad CommandType = 0x30 @@ -148,6 +149,8 @@ func (m *Message) decodePayload() error { p = block.New(m.Network, m.StateRootInHeader) case CMDConsensus: p = consensus.NewPayload(m.Network, m.StateRootInHeader) + case CMDP2PNotaryRequest: + p = &payload.P2PNotaryRequest{} case CMDGetBlocks: p = &payload.GetBlocks{} case CMDGetHeaders: diff --git a/pkg/network/payload/inventory.go b/pkg/network/payload/inventory.go index 988c40eb0f..8727e29292 100644 --- a/pkg/network/payload/inventory.go +++ b/pkg/network/payload/inventory.go @@ -20,21 +20,24 @@ func (i InventoryType) String() string { return "block" case ConsensusType: return "consensus" + case P2PNotaryRequestType: + return "p2pNotaryRequest" default: return "unknown inventory type" } } // Valid returns true if the inventory (type) is known. -func (i InventoryType) Valid() bool { - return i == BlockType || i == TXType || i == ConsensusType +func (i InventoryType) Valid(p2pSigExtensionsEnabled bool) bool { + return i == BlockType || i == TXType || i == ConsensusType || (p2pSigExtensionsEnabled && i == P2PNotaryRequestType) } // List of valid InventoryTypes. const ( - TXType InventoryType = 0x2b - BlockType InventoryType = 0x2c - ConsensusType InventoryType = 0x2d + TXType InventoryType = 0x2b + BlockType InventoryType = 0x2c + ConsensusType InventoryType = 0x2d + P2PNotaryRequestType InventoryType = 0x50 ) // Inventory payload. diff --git a/pkg/network/payload/notary_request.go b/pkg/network/payload/notary_request.go new file mode 100644 index 0000000000..0e11d5610b --- /dev/null +++ b/pkg/network/payload/notary_request.go @@ -0,0 +1,150 @@ +package payload + +import ( + "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" +) + +// 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() + 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.DecodeBinary(br) + r.FallbackTransaction.DecodeBinary(br) + r.Network = netmode.Magic(br.ReadU32LE()) + if br.Err != nil { + return + } + br.Err = r.isValid() +} + +// DecodeBinary implements io.Serializable interface. +func (r *P2PNotaryRequest) DecodeBinary(br *io.BinReader) { + r.decodeHashableFields(br) + if br.Err != nil { + return + } + + var b = br.ReadB() + if b != 1 { + br.Err = errors.New("invalid format") + return + } + + r.Witness.DecodeBinary(br) +} + +// encodeHashableFields writes payload to w excluding signature. +func (r *P2PNotaryRequest) encodeHashableFields(bw *io.BinWriter) { + r.MainTransaction.EncodeBinary(bw) + r.MainTransaction.EncodeBinary(bw) + bw.WriteU32LE(uint32(r.Network)) +} + +// EncodeBinary implements Serializable interface. +func (r *P2PNotaryRequest) EncodeBinary(bw *io.BinWriter) { + r.encodeHashableFields(bw) + bw.WriteB(1) + 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") + } + // TODO: check "either doesn't have all witnesses attached (in this case none of them can be multisignature), or it only has a partial multisignature, only one of the two is allowed " + if len(r.FallbackTransaction.Signers) != 2 { + return errors.New("fallback transaction should have two signers") + } + if !r.FallbackTransaction.HasAttribute(transaction.NotValidBeforeT) { + return errors.New("fallback transactions should have NotValidBefore attribute") + } + var conflictsWithMain bool + conflicts := r.FallbackTransaction.GetAttributes(transaction.ConflictsT) + for _, attr := range conflicts { + if attr.Value.(*transaction.Conflicts).Hash.Equals(r.MainTransaction.Hash()) { + conflictsWithMain = true + break + } + } + if !conflictsWithMain { + return errors.New("fallback transaction does not conflicts with the main transaction") + } + nKeysFallback := r.FallbackTransaction.GetAttributes(transaction.NotaryAssistedT) + if len(nKeysFallback) == 0 { + return errors.New("fallback transaction should have NotaryAssisted attribute") + } + if nKeysFallback[0].Value.(*transaction.NotaryAssisted).NKeys != 0 { + return errors.New("fallback transaction should have NKeys = 0") + } + if r.MainTransaction.ValidUntilBlock != r.FallbackTransaction.ValidUntilBlock { + return errors.New("both main and fallback transactions should have the same ValidUntil value") + } + return nil +} diff --git a/pkg/network/payload/notary_request_test.go b/pkg/network/payload/notary_request_test.go new file mode 100644 index 0000000000..7ca42e0eb5 --- /dev/null +++ b/pkg/network/payload/notary_request_test.go @@ -0,0 +1,84 @@ +package payload + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/internal/random" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/stretchr/testify/require" +) + +func TestIsValid(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 tx: missing NotValidBefore attribute": { + MainTransaction: mainTx, + FallbackTransaction: &transaction.Transaction{Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}}}, + }, + "fallback tx: does not conflicts with main tx": { + 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()}}}, + }, + "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()}}, + }, + }, + "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()}}}}, + "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()}}}}, + } + 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()}}, + }, + } + require.NoError(t, p.isValid()) + }) +} diff --git a/pkg/network/payloadpool/notary_request_pool.go b/pkg/network/payloadpool/notary_request_pool.go new file mode 100644 index 0000000000..0b9269cb27 --- /dev/null +++ b/pkg/network/payloadpool/notary_request_pool.go @@ -0,0 +1,199 @@ +package payloadpool + +import ( + "errors" + "fmt" + "math/big" + "sync" + + "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/payload" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// utilityBalanceAndFees stores sender's deposit balance and overall fees of +// sender's transactions which are currently in pool. +type utilityBalanceAndFees struct { + balance *big.Int + feeSum *big.Int +} + +// NotaryRequestPool stores P2PNotaryRequest payloads. +type NotaryRequestPool struct { + lock sync.RWMutex + verifiedMap map[util.Uint256]*payload.P2PNotaryRequest + verifiedHashes []util.Uint256 + fees map[util.Uint160]utilityBalanceAndFees + capacity int +} + +var ErrInsufficientDeposit = errors.New("insufficient deposit amount") + +// NewNotaryRequestPool returns a new NotaryRequestPool struct. +func NewNotaryRequestPool(capacity int) *NotaryRequestPool { + return &NotaryRequestPool{ + verifiedMap: make(map[util.Uint256]*payload.P2PNotaryRequest), + verifiedHashes: make([]util.Uint256, 0, capacity), + fees: make(map[util.Uint160]utilityBalanceAndFees), + capacity: capacity, + } +} + +// HasPayload checks whether payload with the specified hash is in the pool. +func (mp *NotaryRequestPool) HasPayload(h util.Uint256) bool { + mp.lock.RLock() + defer mp.lock.RUnlock() + return mp.containsKey(h) +} + +// GetPayload returns payload with the specified hash if it is in the mempool. +func (mp *NotaryRequestPool) GetPayload(h util.Uint256) *payload.P2PNotaryRequest { + mp.lock.RLock() + defer mp.lock.RUnlock() + return mp.verifiedMap[h] +} + +// PostBlock is a callback to be called from blockchain after new block is persisted. Block transactions are +// passed via txPool. +func (mp *NotaryRequestPool) PostBlock(bc blockchainer.Blockchainer, txPool *mempool.Pool, _ *block.Block) { + mp.lock.Lock() + defer mp.lock.Unlock() + newVerifiedHashes := mp.verifiedHashes[:0] + mp.fees = make(map[util.Uint160]utilityBalanceAndFees) + for _, h := range mp.verifiedHashes { + payload := mp.verifiedMap[h] + if isNotaryRequestStillRelevant(bc, payload, txPool) && mp.tryAddSendersFee(payload.FallbackTransaction, bc, true) { + newVerifiedHashes = append(newVerifiedHashes, payload.Hash()) + } else { + delete(mp.verifiedMap, h) + } + } + mp.verifiedHashes = newVerifiedHashes +} + +// containsKey checks whether payload with the specified hash is already in pool. +func (mp *NotaryRequestPool) containsKey(hash util.Uint256) bool { + _, ok := mp.verifiedMap[hash] + return ok +} + +// VerifyAndPoolPayload is a callback method, need to be called under the blockchain lock. +func (mp *NotaryRequestPool) VerifyAndPoolPayload(bc blockchainer.Blockchainer, r *payload.P2PNotaryRequest) error { + payer := r.FallbackTransaction.Signers[1].Account + if err := bc.VerifyWitness(payer, r, &r.Witness, bc.GetMaxVerificationGas()); err != nil { + return fmt.Errorf("bad P2PNotaryRequest payload witness: %w", err) + } + if r.FallbackTransaction.Sender() != bc.NotaryContractHash() { + return errors.New("P2PNotary contract should be a sender of the fallback transaction") + } + if err := bc.VerifyWitness(payer, r.FallbackTransaction, &r.FallbackTransaction.Scripts[1], bc.GetMaxVerificationGas()); err != nil { + return fmt.Errorf("bad fallback transaction witness #1: %s", err.Error()) + } + nvbFallback := r.FallbackTransaction.GetAttributes(transaction.NotValidBeforeT)[0].Value.(*transaction.NotValidBefore).Height + maxNVBDelta := uint32(bc.GetMaxNotValidBeforeDelta()) + if bc.BlockHeight()+maxNVBDelta < nvbFallback { + return fmt.Errorf("fallback transaction should have NotValidBefore not more then %d, got %d", bc.BlockHeight()+maxNVBDelta, nvbFallback) + } + if nvbFallback+maxNVBDelta < r.FallbackTransaction.ValidUntilBlock { + return fmt.Errorf("payload should be valid at least %d blocks after fallback's transaction NotValidBefore", maxNVBDelta) + } + depositExpiration := bc.ExpirationOf(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 mp.add(r, bc) +} + +// Add tries to add given payload to the NotaryRequestPool. +func (mp *NotaryRequestPool) add(r *payload.P2PNotaryRequest, bc blockchainer.Blockchainer) error { + mp.lock.Lock() + defer mp.lock.Unlock() + + if mp.containsKey(r.Hash()) { + return errors.New("payload already in pool") + } + + err := mp.checkPayloadConflicts(r, bc) + if err != nil { + return fmt.Errorf("invalid P2PNotaryRequestPayload: %s", err.Error()) + } + + mp.verifiedMap[r.Hash()] = r + if len(mp.verifiedHashes) == mp.capacity { + delete(mp.verifiedMap, mp.verifiedHashes[0]) + copy(mp.verifiedHashes, mp.verifiedHashes[1:]) + mp.verifiedHashes[mp.capacity-1] = r.Hash() + } else { + mp.verifiedHashes = append(mp.verifiedHashes, r.Hash()) + } + + // we already checked balance in checkPayloadConflicts, so don't need to check again + mp.tryAddSendersFee(r.FallbackTransaction, bc, false) + return nil +} + +// tryAddSendersFee tries to add system fee and network fee to the total payer`s fee in mempool +// and returns false if both balance check is required and payer has not enough deposited GAS to pay. +func (mp *NotaryRequestPool) tryAddSendersFee(tx *transaction.Transaction, bc blockchainer.Blockchainer, needCheck bool) bool { + payer := tx.Signers[1].Account + payerFee, ok := mp.fees[payer] + if !ok { + payerFee.balance = bc.GetDepositFor(payer) + payerFee.feeSum = big.NewInt(0) + mp.fees[payer] = payerFee + } + if needCheck { + newFeeSum, err := checkBalance(tx, payerFee) + if err != nil { + return false + } + payerFee.feeSum.Set(newFeeSum) + } else { + payerFee.feeSum.Add(payerFee.feeSum, big.NewInt(tx.SystemFee+tx.NetworkFee)) + } + return true +} + +// checkBalance returns new cumulative fee balance for account or an error in +// case sender doesn't have enough deposited GAS to pay for the transaction. +func checkBalance(tx *transaction.Transaction, balance utilityBalanceAndFees) (*big.Int, error) { + txFee := big.NewInt(tx.SystemFee + tx.NetworkFee) + if balance.balance.Cmp(txFee) < 0 { + return nil, ErrInsufficientDeposit + } + txFee.Add(txFee, balance.feeSum) + if balance.balance.Cmp(txFee) < 0 { + return nil, ErrInsufficientDeposit + } + return txFee, nil +} + +// checkPayloadConflicts checks whether payload sender has enough deposited GAS to pay for all fallback transactions +// which are currently in the pool. +func (mp *NotaryRequestPool) checkPayloadConflicts(r *payload.P2PNotaryRequest, bc blockchainer.Blockchainer) error { + payer := r.FallbackTransaction.Signers[1].Account + payerFee, ok := mp.fees[payer] + if !ok { + payerFee.balance = bc.GetDepositFor(payer) + payerFee.feeSum = big.NewInt(0) + } + _, err := checkBalance(r.FallbackTransaction, payerFee) + return err +} + +// isNotaryRequestStillRelevant checks whether payload is still valid. +func isNotaryRequestStillRelevant(bc blockchainer.Blockchainer, r *payload.P2PNotaryRequest, txpool *mempool.Pool) bool { + if r.MainTransaction.ValidUntilBlock <= bc.BlockHeight() { + return false + } + if txpool.ContainsKey(r.MainTransaction.Hash()) { + return false + } + if txpool.ContainsKey(r.FallbackTransaction.Hash()) { + return false + } + return true +} diff --git a/pkg/network/payloadpool/notary_request_pool_test.go b/pkg/network/payloadpool/notary_request_pool_test.go new file mode 100644 index 0000000000..aaddc14b97 --- /dev/null +++ b/pkg/network/payloadpool/notary_request_pool_test.go @@ -0,0 +1,64 @@ +package payloadpool + +/* + +func TestIsNotaryRequestPayloadStillRelevant(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + mp := bc.notaryRequestPool + require.NotNil(t, mp) + + newTx := func(t *testing.T) *transaction.Transaction { + tx := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.RET)}, 100) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.Signers = []transaction.Signer{{ + Account: neoOwner, + Scopes: transaction.CalledByEntry, + }} + return tx + } + + t.Run("expired payload", func(t *testing.T) { + require.NoError(t, bc.AddBlock(bc.newBlock())) + mainTx := newTx(t) + mainTx.ValidUntilBlock = bc.BlockHeight() + require.False(t, bc.isNotaryRequestStillRelevant(&payload.P2PNotaryRequest{ + MainTransaction: mainTx, + }, nil)) + }) + t.Run("mainTx is in persisted block", func(t *testing.T) { + txPool := mempool.New(1) + mainTx := newTx(t) + addSigners(mainTx) + require.NoError(t, testchain.SignTx(bc, mainTx)) + require.NoError(t, bc.verifyAndPoolTx(mainTx, txPool)) + + require.False(t, bc.isNotaryRequestStillRelevant(&payload.P2PNotaryRequest{ + MainTransaction: mainTx, + }, txPool)) + }) + t.Run("fallbackTx is in persisted block", func(t *testing.T) { + txPool := mempool.New(1) + fallbackTx := newTx(t) + addSigners(fallbackTx) + require.NoError(t, testchain.SignTx(bc, fallbackTx)) + require.NoError(t, bc.verifyAndPoolTx(fallbackTx, txPool)) + + require.False(t, bc.isNotaryRequestStillRelevant(&payload.P2PNotaryRequest{ + FallbackTransaction: fallbackTx, + MainTransaction: newTx(t), + }, txPool)) + }) + t.Run("good", func(t *testing.T) { + txPool := mempool.New(1) + fallbackTx := newTx(t) + mainTx := newTx(t) + + require.True(t, bc.isNotaryRequestStillRelevant(&payload.P2PNotaryRequest{ + FallbackTransaction: fallbackTx, + MainTransaction: mainTx, + }, txPool)) + }) +} +*/ diff --git a/pkg/network/server.go b/pkg/network/server.go index ed184e86f5..00e522f66d 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -18,6 +18,7 @@ import ( "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" + "github.com/nspcc-dev/neo-go/pkg/network/payloadpool" "github.com/nspcc-dev/neo-go/pkg/util" "go.uber.org/atomic" "go.uber.org/zap" @@ -58,11 +59,12 @@ type ( // stateRootInHeader specifies if block header contain state root. stateRootInHeader bool - transport Transporter - discovery Discoverer - chain blockchainer.Blockchainer - bQueue *blockQueue - consensus consensus.Service + transport Transporter + discovery Discoverer + chain blockchainer.Blockchainer + bQueue *blockQueue + consensus consensus.Service + notaryRequestPool *payloadpool.NotaryRequestPool lock sync.RWMutex peers map[Peer]bool @@ -110,6 +112,11 @@ func NewServer(config ServerConfig, chain blockchainer.Blockchainer, log *zap.Lo log: log, transactions: make(chan *transaction.Transaction, 64), } + if chain.P2PSigExtensionsEnabled() { + s.notaryRequestPool = payloadpool.NewNotaryRequestPool(chain.GetConfig().P2PNotaryRequestPayloadPoolSize) + chain.RegisterPostBlock(s.notaryRequestPool.PostBlock) + chain.RegisterPoolNotaryRequestCallback(s.notaryRequestPool.VerifyAndPoolPayload) + } s.bQueue = newBlockQueue(maxBlockBatch, chain, log, func(b *block.Block) { if !s.consensusStarted.Load() { s.tryStartConsensus() @@ -493,6 +500,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.HasPayload(h) + }, } if exists := typExists[inv.Type]; exists != nil { for _, hash := range inv.Hashes { @@ -559,6 +569,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 := s.notaryRequestPool.GetPayload(hash); nrp != nil { // already have checked P2PSigExtEnabled + msg = NewMessage(CMDP2PNotaryRequest, nrp) + } else { + notFound = append(notFound, hash) + } } if msg != nil { pkt, err := msg.Bytes() @@ -678,6 +694,20 @@ func (s *Server) handleTxCmd(tx *transaction.Transaction) error { 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 err := s.chain.PoolP2PNotaryRequestPayload(r); err != nil { + return err + } + + msg := NewMessage(CMDInv, payload.NewInventory(payload.P2PNotaryRequestType, []util.Uint256{r.Hash()})) // TODO: retransmission + s.broadcastMessage(msg) + return nil +} + // handleAddrCmd will process received addresses. func (s *Server) handleAddrCmd(p Peer, addrs *payload.AddressList) error { if !p.CanProcessAddr() { @@ -724,7 +754,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 } } @@ -762,6 +792,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)