Merge pull request #1658 from nspcc-dev/signature_collection/module

core, network: add Notary module
This commit is contained in:
Roman Khimov 2021-02-02 22:24:14 +03:00 committed by GitHub
commit 641896b2fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 2429 additions and 372 deletions

View 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")
}

View 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"`
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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,8 +417,8 @@ 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{
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,
@ -386,7 +426,14 @@ func New(capacity int, payerIndex int) *Pool {
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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

View 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
)

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

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