Compare commits

...

4 commits

Author SHA1 Message Date
Anna Shaleva
c49dba2db0 notary: add an example of attack on notary service 2023-07-19 19:24:21 +03:00
Anna Shaleva
133082ed58 core: check signers of on-chained conflict during new tx verification
During new transaction verification if there's an on-chain conflicting
transaction, we should check the signers of this conflicting transaction.
If the signers intersect with signers of the incoming transaction, then
the conflict is treated as valid and verification for new incoming
transaction should fail. Otherwise, the conflict is treated as the
malicious attack attempt and will not be taken into account;
verification for the new incoming transaction should continue.

This commint implements the scheme described at
https://github.com/neo-project/neo/pull/2818#issuecomment-1632972055,
thanks to @shargon for digging.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-07-19 14:52:59 +03:00
Anna Shaleva
cf4001d31c core: fix formatted error on transaction verification
Witnesses are not yet created by the moment we return this error,
thus, it was always 0 as an actual number of witnesses in
ErrInvalidWitnessNum.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-07-14 11:42:27 +03:00
Anna Shaleva
edd4f390ad core: remove unused blockchain API
`(*Blockchain).HasTransaction` is one of the oldest methods in our
codebase, and currently it's completely unused. I also doubt that
this method works as expected because it returns `true` if transaction
in the mempool.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
2023-07-14 10:53:35 +03:00
7 changed files with 449 additions and 58 deletions

View file

@ -45,7 +45,7 @@ import (
// Tuning parameters. // Tuning parameters.
const ( const (
version = "0.2.8" version = "0.2.9"
defaultInitialGAS = 52000000_00000000 defaultInitialGAS = 52000000_00000000
defaultGCPeriod = 10000 defaultGCPeriod = 10000
@ -2174,15 +2174,6 @@ func (bc *Blockchain) GetHeader(hash util.Uint256) (*block.Header, error) {
return &block.Header, nil return &block.Header, nil
} }
// HasTransaction returns true if the blockchain contains he given
// transaction hash.
func (bc *Blockchain) HasTransaction(hash util.Uint256) bool {
if bc.memPool.ContainsKey(hash) {
return true
}
return errors.Is(bc.dao.HasTransaction(hash), dao.ErrAlreadyExists)
}
// HasBlock returns true if the blockchain contains the given // HasBlock returns true if the blockchain contains the given
// block hash. // block hash.
func (bc *Blockchain) HasBlock(hash util.Uint256) bool { func (bc *Blockchain) HasBlock(hash util.Uint256) bool {
@ -2486,7 +2477,11 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee) return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
} }
// check that current tx wasn't included in the conflicts attributes of some other transaction which is already in the chain // check that current tx wasn't included in the conflicts attributes of some other transaction which is already in the chain
if err := bc.dao.HasTransaction(t.Hash()); err != nil { var signers = make(map[util.Uint160]struct{}, len(t.Signers))
for _, s := range t.Signers {
signers[s.Account] = struct{}{}
}
if err := bc.dao.HasTransaction(t.Hash(), signers); err != nil {
switch { switch {
case errors.Is(err, dao.ErrAlreadyExists): case errors.Is(err, dao.ErrAlreadyExists):
return fmt.Errorf("blockchain: %w", ErrAlreadyExists) return fmt.Errorf("blockchain: %w", ErrAlreadyExists)
@ -2587,7 +2582,9 @@ func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transact
return fmt.Errorf("%w: Conflicts attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute) return fmt.Errorf("%w: Conflicts attribute was found, but P2PSigExtensions are disabled", ErrInvalidAttribute)
} }
conflicts := tx.Attributes[i].Value.(*transaction.Conflicts) conflicts := tx.Attributes[i].Value.(*transaction.Conflicts)
if err := bc.dao.HasTransaction(conflicts.Hash); errors.Is(err, dao.ErrAlreadyExists) { // Only fully-qualified dao.ErrAlreadyExists error bothers us here, thus, we
// can safely omit the payer argument to HasTransaction call to improve performance a bit.
if err := bc.dao.HasTransaction(conflicts.Hash, nil); errors.Is(err, dao.ErrAlreadyExists) {
return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE()) return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE())
} }
case transaction.NotaryAssistedT: case transaction.NotaryAssistedT:
@ -2620,7 +2617,11 @@ func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *memp
return false return false
} }
if txpool == nil { if txpool == nil {
if bc.dao.HasTransaction(t.Hash()) != nil { var signers = make(map[util.Uint160]struct{}, len(t.Signers))
for _, s := range t.Signers {
signers[s.Account] = struct{}{}
}
if bc.dao.HasTransaction(t.Hash(), signers) != nil {
return false return false
} }
} else if txpool.HasConflicts(t, bc) { } else if txpool.HasConflicts(t, bc) {

View file

@ -1634,28 +1634,159 @@ func TestBlockchain_VerifyTx(t *testing.T) {
}) })
t.Run("enabled", func(t *testing.T) { t.Run("enabled", func(t *testing.T) {
t.Run("dummy on-chain conflict", func(t *testing.T) { t.Run("dummy on-chain conflict", func(t *testing.T) {
tx := newTestTx(t, h, testScript) t.Run("on-chain conflict signed by malicious party", func(t *testing.T) {
require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx)) tx := newTestTx(t, h, testScript)
conflicting := transaction.New([]byte{byte(opcode.RET)}, 1000_0000) require.NoError(t, accs[0].SignTx(netmode.UnitTestNet, tx))
conflicting.ValidUntilBlock = bc.BlockHeight() + 1 conflicting := transaction.New([]byte{byte(opcode.RET)}, 1000_0000)
conflicting.Signers = []transaction.Signer{ conflicting.ValidUntilBlock = bc.BlockHeight() + 1
{ conflicting.Signers = []transaction.Signer{
Account: validator.ScriptHash(), {
Scopes: transaction.CalledByEntry, Account: validator.ScriptHash(),
}, Scopes: transaction.CalledByEntry,
}
conflicting.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: tx.Hash(),
}, },
}, }
} conflicting.Attributes = []transaction.Attribute{
conflicting.NetworkFee = 1000_0000 {
require.NoError(t, validator.SignTx(netmode.UnitTestNet, conflicting)) Type: transaction.ConflictsT,
e.AddNewBlock(t, conflicting) Value: &transaction.Conflicts{
require.ErrorIs(t, bc.VerifyTx(tx), core.ErrHasConflicts) Hash: tx.Hash(),
},
},
}
conflicting.NetworkFee = 1000_0000
require.NoError(t, validator.SignTx(netmode.UnitTestNet, conflicting))
e.AddNewBlock(t, conflicting)
// We expect `tx` to pass verification, because on-chained `conflicting` doesn't have
// `tx`'s payer in the signers list, thus, `confclicting` should be considered as
// malicious conflict.
require.NoError(t, bc.VerifyTx(tx))
})
t.Run("multiple on-chain conflicts signed by malicious parties", func(t *testing.T) {
m1 := e.NewAccount(t)
m2 := e.NewAccount(t)
m3 := e.NewAccount(t)
good := e.NewAccount(t)
// txGood doesn't conflivt with anyone and signed by good signer.
txGood := newTestTx(t, good.ScriptHash(), testScript)
require.NoError(t, good.SignTx(netmode.UnitTestNet, txGood))
// txM1 conflicts with txGood and signed by two malicious signers.
txM1 := newTestTx(t, m1.ScriptHash(), testScript)
txM1.Signers = append(txM1.Signers, transaction.Signer{Account: m2.ScriptHash()})
txM1.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: txGood.Hash(),
},
},
}
txM1.NetworkFee = 1_000_0000
require.NoError(t, m1.SignTx(netmode.UnitTestNet, txM1))
require.NoError(t, m2.SignTx(netmode.UnitTestNet, txM1))
e.AddNewBlock(t, txM1)
// txM2 conflicts with txGood and signed by one malicious signers.
txM2 := newTestTx(t, m3.ScriptHash(), testScript)
txM2.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: txGood.Hash(),
},
},
}
txM2.NetworkFee = 1_000_0000
require.NoError(t, m3.SignTx(netmode.UnitTestNet, txM2))
e.AddNewBlock(t, txM2)
// We expect `tx` to pass verification, because on-chained `conflicting` doesn't have
// `tx`'s payer in the signers list, thus, `confclicting` should be considered as
// malicious conflict.
require.NoError(t, bc.VerifyTx(txGood))
// After that txGood can be added to the chain normally.
e.AddNewBlock(t, txGood)
// And after that ErrAlreadyExist is expected on verification.
require.ErrorIs(t, bc.VerifyTx(txGood), core.ErrAlreadyExists)
})
t.Run("multiple on-chain conflicts signed by malicious and valid parties", func(t *testing.T) {
m1 := e.NewAccount(t)
m2 := e.NewAccount(t)
m3 := e.NewAccount(t)
good := e.NewAccount(t)
// txGood doesn't conflivt with anyone and signed by good signer.
txGood := newTestTx(t, good.ScriptHash(), testScript)
require.NoError(t, good.SignTx(netmode.UnitTestNet, txGood))
// txM1 conflicts with txGood and signed by one malicious and one good signers.
txM1 := newTestTx(t, m1.ScriptHash(), testScript)
txM1.Signers = append(txM1.Signers, transaction.Signer{Account: good.ScriptHash()})
txM1.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: txGood.Hash(),
},
},
}
txM1.NetworkFee = 1_000_0000
require.NoError(t, m1.SignTx(netmode.UnitTestNet, txM1))
require.NoError(t, good.SignTx(netmode.UnitTestNet, txM1))
e.AddNewBlock(t, txM1)
// txM2 conflicts with txGood and signed by two malicious signers.
txM2 := newTestTx(t, m2.ScriptHash(), testScript)
txM2.Signers = append(txM2.Signers, transaction.Signer{Account: m3.ScriptHash()})
txM2.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: txGood.Hash(),
},
},
}
txM2.NetworkFee = 1_000_0000
require.NoError(t, m2.SignTx(netmode.UnitTestNet, txM2))
require.NoError(t, m3.SignTx(netmode.UnitTestNet, txM2))
e.AddNewBlock(t, txM2)
// We expect `tx` to pass verification, because on-chained `conflicting` doesn't have
// `tx`'s payer in the signers list, thus, `confclicting` should be considered as
// malicious conflict.
require.ErrorIs(t, bc.VerifyTx(txGood), core.ErrHasConflicts)
})
t.Run("on-chain conflict signed by valid sender", func(t *testing.T) {
tx := newTestTx(t, h, testScript)
tx.Signers = []transaction.Signer{{Account: validator.ScriptHash()}}
require.NoError(t, validator.SignTx(netmode.UnitTestNet, tx))
conflicting := transaction.New([]byte{byte(opcode.RET)}, 1000_0000)
conflicting.ValidUntilBlock = bc.BlockHeight() + 1
conflicting.Signers = []transaction.Signer{
{
Account: validator.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
}
conflicting.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: tx.Hash(),
},
},
}
conflicting.NetworkFee = 1000_0000
require.NoError(t, validator.SignTx(netmode.UnitTestNet, conflicting))
e.AddNewBlock(t, conflicting)
// We expect `tx` to fail verification, because on-chained `conflicting` has
// `tx`'s payer as a signer.
require.ErrorIs(t, bc.VerifyTx(tx), core.ErrHasConflicts)
})
}) })
t.Run("attribute on-chain conflict", func(t *testing.T) { t.Run("attribute on-chain conflict", func(t *testing.T) {
tx := neoValidatorsInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoOwner, neoOwner, 1, nil) tx := neoValidatorsInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoOwner, neoOwner, 1, nil)

View file

@ -684,8 +684,11 @@ func (dao *Simple) StoreHeaderHashes(hashes []util.Uint256, height uint32) error
// HasTransaction returns nil if the given store does not contain the given // HasTransaction returns nil if the given store does not contain the given
// Transaction hash. It returns an error in case the transaction is in chain // Transaction hash. It returns an error in case the transaction is in chain
// or in the list of conflicting transactions. // or in the list of conflicting transactions. If non-zero signers are specified,
func (dao *Simple) HasTransaction(hash util.Uint256) error { // then additional check against the conflicting transaction signers intersection
// is held. Do not omit signers in case if it's important to check the validity
// of a supposedly conflicting on-chain transaction.
func (dao *Simple) HasTransaction(hash util.Uint256, signers map[util.Uint160]struct{}) error {
key := dao.makeExecutableKey(hash) key := dao.makeExecutableKey(hash)
bytes, err := dao.Store.Get(key) bytes, err := dao.Store.Get(key)
if err != nil { if err != nil {
@ -695,10 +698,33 @@ func (dao *Simple) HasTransaction(hash util.Uint256) error {
if len(bytes) < 6 { if len(bytes) < 6 {
return nil return nil
} }
if bytes[5] == transaction.DummyVersion { if bytes[5] != transaction.DummyVersion {
return ErrAlreadyExists
}
if len(signers) == 0 {
return ErrHasConflicts return ErrHasConflicts
} }
return ErrAlreadyExists
var conflictTxSigners []util.Uint160
br := io.NewBinReaderFromBuf(bytes[6:])
for {
var u util.Uint160
u.DecodeBinary(br)
if br.Err != nil {
if errors.Is(br.Err, iocore.EOF) {
break
}
return fmt.Errorf("failed to decode conflict record: %w", err)
}
conflictTxSigners = append(conflictTxSigners, u)
}
for _, s := range conflictTxSigners {
if _, ok := signers[s]; ok {
return ErrHasConflicts
}
}
return nil
} }
// StoreAsBlock stores given block as DataBlock. It can reuse given buffer for // StoreAsBlock stores given block as DataBlock. It can reuse given buffer for
@ -805,17 +831,45 @@ func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32,
} }
dao.Store.Put(key, buf.Bytes()) dao.Store.Put(key, buf.Bytes())
if dao.Version.P2PSigExtensions { if dao.Version.P2PSigExtensions {
var value []byte var (
valuePrefix []byte
newSigners []byte
)
for _, attr := range tx.GetAttributes(transaction.ConflictsT) { for _, attr := range tx.GetAttributes(transaction.ConflictsT) {
hash := attr.Value.(*transaction.Conflicts).Hash hash := attr.Value.(*transaction.Conflicts).Hash
copy(key[1:], hash.BytesBE()) copy(key[1:], hash.BytesBE())
if value == nil {
var oldSigners []byte
old, err := dao.Store.Get(key)
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
return fmt.Errorf("failed to retrieve previous conflict record for %s: %w", hash.StringLE(), err)
}
if err == nil {
if len(old) <= 6 { // storage.ExecTransaction + U32LE index + transaction.DummyVersion
return fmt.Errorf("invalid conflict record format of length %d", len(old))
}
valuePrefix = old[:6]
oldSigners = old[6:]
}
if valuePrefix == nil {
buf.Reset() buf.Reset()
buf.WriteB(storage.ExecTransaction) buf.WriteB(storage.ExecTransaction)
buf.WriteU32LE(index) buf.WriteU32LE(index)
buf.BinWriter.WriteB(transaction.DummyVersion) buf.BinWriter.WriteB(transaction.DummyVersion)
value = buf.Bytes() b := buf.Bytes()
valuePrefix = make([]byte, len(b))
copy(valuePrefix, b)
} }
if newSigners == nil {
buf.Reset()
for _, s := range tx.Signers {
s.Account.EncodeBinary(buf.BinWriter)
}
b := buf.Bytes()
newSigners = make([]byte, len(b))
copy(newSigners, b)
}
value := append(valuePrefix, append(oldSigners, newSigners...)...)
dao.Store.Put(key, value) dao.Store.Put(key, value)
} }
} }

View file

@ -186,8 +186,8 @@ func TestStoreAsTransaction(t *testing.T) {
} }
err := dao.StoreAsTransaction(tx, 0, aer) err := dao.StoreAsTransaction(tx, 0, aer)
require.NoError(t, err) require.NoError(t, err)
err = dao.HasTransaction(hash) err = dao.HasTransaction(hash, nil)
require.NotNil(t, err) require.ErrorIs(t, err, ErrAlreadyExists)
gotAppExecResult, err := dao.GetAppExecResults(hash, trigger.All) gotAppExecResult, err := dao.GetAppExecResults(hash, trigger.All)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(gotAppExecResult)) require.Equal(t, 1, len(gotAppExecResult))
@ -197,34 +197,84 @@ func TestStoreAsTransaction(t *testing.T) {
t.Run("P2PSigExtensions on", func(t *testing.T) { t.Run("P2PSigExtensions on", func(t *testing.T) {
dao := NewSimple(storage.NewMemoryStore(), false, true) dao := NewSimple(storage.NewMemoryStore(), false, true)
conflictsH := util.Uint256{1, 2, 3} conflictsH := util.Uint256{1, 2, 3}
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 1) signer1 := util.Uint160{1, 2, 3}
tx.Signers = append(tx.Signers, transaction.Signer{}) signer2 := util.Uint160{4, 5, 6}
tx.Scripts = append(tx.Scripts, transaction.Witness{}) signer3 := util.Uint160{7, 8, 9}
tx.Attributes = []transaction.Attribute{ signerMalicious := util.Uint160{10, 11, 12}
tx1 := transaction.New([]byte{byte(opcode.PUSH1)}, 1)
tx1.Signers = append(tx1.Signers, transaction.Signer{Account: signer1}, transaction.Signer{Account: signer2})
tx1.Scripts = append(tx1.Scripts, transaction.Witness{}, transaction.Witness{})
tx1.Attributes = []transaction.Attribute{
{ {
Type: transaction.ConflictsT, Type: transaction.ConflictsT,
Value: &transaction.Conflicts{Hash: conflictsH}, Value: &transaction.Conflicts{Hash: conflictsH},
}, },
} }
hash := tx.Hash() hash1 := tx1.Hash()
aer := &state.AppExecResult{ tx2 := transaction.New([]byte{byte(opcode.PUSH1)}, 1)
Container: hash, tx2.Signers = append(tx2.Signers, transaction.Signer{Account: signer3})
tx2.Scripts = append(tx2.Scripts, transaction.Witness{})
tx2.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{Hash: conflictsH},
},
}
hash2 := tx2.Hash()
aer1 := &state.AppExecResult{
Container: hash1,
Execution: state.Execution{ Execution: state.Execution{
Trigger: trigger.Application, Trigger: trigger.Application,
Events: []state.NotificationEvent{}, Events: []state.NotificationEvent{},
Stack: []stackitem.Item{}, Stack: []stackitem.Item{},
}, },
} }
err := dao.StoreAsTransaction(tx, 0, aer) err := dao.StoreAsTransaction(tx1, 0, aer1)
require.NoError(t, err) require.NoError(t, err)
err = dao.HasTransaction(hash) aer2 := &state.AppExecResult{
Container: hash2,
Execution: state.Execution{
Trigger: trigger.Application,
Events: []state.NotificationEvent{},
Stack: []stackitem.Item{},
},
}
err = dao.StoreAsTransaction(tx2, 0, aer2)
require.NoError(t, err)
err = dao.HasTransaction(hash1, nil)
require.ErrorIs(t, err, ErrAlreadyExists) require.ErrorIs(t, err, ErrAlreadyExists)
err = dao.HasTransaction(conflictsH) err = dao.HasTransaction(hash2, nil)
require.ErrorIs(t, err, ErrAlreadyExists)
// Conflicts: unimportant payer.
err = dao.HasTransaction(conflictsH, nil)
require.ErrorIs(t, err, ErrHasConflicts) require.ErrorIs(t, err, ErrHasConflicts)
gotAppExecResult, err := dao.GetAppExecResults(hash, trigger.All)
// Conflicts: payer is important, conflict isn't malicious, test signer #1.
err = dao.HasTransaction(conflictsH, map[util.Uint160]struct{}{signer1: {}})
require.ErrorIs(t, err, ErrHasConflicts)
// Conflicts: payer is important, conflict isn't malicious, test signer #2.
err = dao.HasTransaction(conflictsH, map[util.Uint160]struct{}{signer2: {}})
require.ErrorIs(t, err, ErrHasConflicts)
// Conflicts: payer is important, conflict isn't malicious, test signer #3.
err = dao.HasTransaction(conflictsH, map[util.Uint160]struct{}{signer3: {}})
require.ErrorIs(t, err, ErrHasConflicts)
// Conflicts: payer is important, conflict is malicious.
err = dao.HasTransaction(conflictsH, map[util.Uint160]struct{}{signerMalicious: {}})
require.NoError(t, err)
gotAppExecResult, err := dao.GetAppExecResults(hash1, trigger.All)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(gotAppExecResult)) require.Equal(t, 1, len(gotAppExecResult))
require.Equal(t, *aer, gotAppExecResult[0]) require.Equal(t, *aer1, gotAppExecResult[0])
gotAppExecResult, err = dao.GetAppExecResults(hash2, trigger.All)
require.NoError(t, err)
require.Equal(t, 1, len(gotAppExecResult))
require.Equal(t, *aer2, gotAppExecResult[0])
}) })
} }

View file

@ -542,8 +542,19 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
if !ok { if !ok {
continue continue
} }
if !tx.HasSigner(existingTx.Signers[mp.payerIndex].Account) { signers := make(map[util.Uint160]struct{}, len(existingTx.Signers))
return nil, fmt.Errorf("%w: not signed by the sender of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE()) for _, s := range existingTx.Signers {
signers[s.Account] = struct{}{}
}
var signerOK bool
for _, s := range tx.Signers {
if _, ok := signers[s.Account]; ok {
signerOK = true
break
}
}
if !signerOK {
return nil, fmt.Errorf("%w: not signed by a signer of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE())
} }
conflictingFee += existingTx.NetworkFee conflictingFee += existingTx.NetworkFee
conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx) conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx)

View file

@ -193,7 +193,7 @@ func (t *Transaction) decodeBinaryNoSize(br *io.BinReader, buf []byte) {
br.Err = errors.New("too many witnesses") br.Err = errors.New("too many witnesses")
return return
} else if int(nscripts) != len(t.Signers) { } else if int(nscripts) != len(t.Signers) {
br.Err = fmt.Errorf("%w: %d vs %d", ErrInvalidWitnessNum, len(t.Signers), len(t.Scripts)) br.Err = fmt.Errorf("%w: %d vs %d", ErrInvalidWitnessNum, len(t.Signers), int(nscripts))
return return
} }
t.Scripts = make([]Witness, nscripts) t.Scripts = make([]Witness, nscripts)

View file

@ -3,6 +3,7 @@ package notary_test
import ( import (
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"math/rand" "math/rand"
"sync" "sync"
@ -747,3 +748,146 @@ func TestNotary(t *testing.T) {
}, 3*time.Second, 100*time.Millisecond) }, 3*time.Second, 100*time.Millisecond)
checkFallbackTxs(t, requests, false) checkFallbackTxs(t, requests, false)
} }
func TestNotaryAttack_Case2(t *testing.T) {
bc, validators, committee := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) { c.P2PSigExtensions = true })
e := neotest.NewExecutor(t, bc, validators, committee)
notaryHash := e.NativeHash(t, nativenames.Notary)
designationSuperInvoker := e.NewInvoker(e.NativeHash(t, nativenames.Designation), validators, committee)
var maliciousFallbackFinilized *transaction.Transaction
onTransaction := func(tx *transaction.Transaction) error {
fmt.Printf("\n\n\nMalicious fallback %s sent to chain!\n\n\n", tx.Hash().StringLE())
maliciousFallbackFinilized = tx
return nil
}
// Start notary service.
acc1, ntr1, mp1 := getTestNotary(t, bc, "./testdata/notary1.json", "one", onTransaction)
bc.SetNotary(ntr1)
bc.RegisterPostBlock(func(f func(*transaction.Transaction, *mempool.Pool, bool) bool, pool *mempool.Pool, b *block.Block) {
ntr1.PostPersist()
})
mp1.RunSubscriptions()
ntr1.Start()
t.Cleanup(func() {
ntr1.Shutdown()
mp1.StopSubscriptions()
})
// Designate notary node.
notaryNodes := []any{acc1.PublicKey().Bytes()}
designationSuperInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int64(noderoles.P2PNotary), notaryNodes)
// Good signer is just a good signer trying to send notary request; bad signer is
// malicious and tries to ruin the notary nodes reward for the good signer's request.
goodSigner := e.NewAccount(t)
badSigner := e.NewAccount(t)
// Make notary deposit.
gasGoodInv := e.NewInvoker(e.NativeHash(t, nativenames.Gas), goodSigner)
gasBadInv := e.NewInvoker(e.NativeHash(t, nativenames.Gas), badSigner)
gasGoodInv.Invoke(t, true, "transfer", goodSigner.ScriptHash(), notaryHash, 3_0000_0000, []interface{}{goodSigner.ScriptHash(), math.MaxUint32})
gasBadInv.Invoke(t, true, "transfer", badSigner.ScriptHash(), notaryHash, 3_0000_0000, []interface{}{badSigner.ScriptHash(), math.MaxUint32})
// Create good notary request.
mainTx := &transaction.Transaction{
Nonce: rand.Uint32(),
SystemFee: 1_0000_0000,
NetworkFee: 1_0000_0000,
ValidUntilBlock: bc.BlockHeight() + 100,
Script: []byte{byte(opcode.RET)},
Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
Signers: []transaction.Signer{
{Account: goodSigner.ScriptHash()},
{Account: notaryHash},
},
Scripts: []transaction.Witness{
{
InvocationScript: []byte{}, // Pretend it will be filled later to simplify the test.
VerificationScript: goodSigner.Script(),
},
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, 64)...),
VerificationScript: []byte{},
},
},
}
fallbackTx := &transaction.Transaction{
Nonce: rand.Uint32(),
SystemFee: 1_0000_0000,
NetworkFee: 1_0000_0000,
ValidUntilBlock: bc.BlockHeight() + 100,
Script: []byte{byte(opcode.RET)},
Attributes: []transaction.Attribute{
{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: bc.BlockHeight() + 50}},
{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
},
Signers: []transaction.Signer{
{Account: notaryHash},
{Account: goodSigner.ScriptHash()},
},
Scripts: []transaction.Witness{
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, 64)...),
VerificationScript: []byte{},
},
},
}
require.NoError(t, goodSigner.SignTx(netmode.UnitTestNet, fallbackTx))
goodReq := &payload.P2PNotaryRequest{
MainTransaction: mainTx,
FallbackTransaction: fallbackTx,
}
goodReq.Witness = transaction.Witness{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, goodSigner.SignHashable(uint32(netmode.UnitTestNet), goodReq)...),
VerificationScript: goodSigner.Script(),
}
// Create malicious notary request. Its main transaction is `fallbackTx`,
// and although its main transaction isn't valid, the fallback will be successfully
// finalized and pushed to the chain, which will prevent the good `fallbackTx` from
// entering the chain and break Notary nodes reward scheme.
cp := *fallbackTx
mainBad := &cp
fallbackBad := &transaction.Transaction{
Nonce: rand.Uint32(),
SystemFee: 1_0000_0000,
NetworkFee: 1_0000_0000 + 1,
ValidUntilBlock: bc.BlockHeight() + 100,
Script: []byte{byte(opcode.RET)},
Attributes: []transaction.Attribute{
{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: bc.BlockHeight() + 1}},
{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainBad.Hash()}},
},
Signers: []transaction.Signer{
{Account: notaryHash},
{Account: badSigner.ScriptHash()},
},
Scripts: []transaction.Witness{
{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, 64)...),
VerificationScript: []byte{},
},
},
}
require.NoError(t, badSigner.SignTx(netmode.UnitTestNet, fallbackBad))
badReq := &payload.P2PNotaryRequest{
MainTransaction: mainBad,
FallbackTransaction: fallbackBad,
}
badReq.Witness = transaction.Witness{
InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, badSigner.SignHashable(uint32(netmode.UnitTestNet), badReq)...),
VerificationScript: badSigner.Script(),
}
ntr1.OnNewRequest(goodReq)
ntr1.OnNewRequest(badReq)
e.AddNewBlock(t)
e.AddNewBlock(t)
require.NotNil(t, maliciousFallbackFinilized)
e.AddNewBlock(t, maliciousFallbackFinilized)
}