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