Compare commits
4 commits
master
...
notary-att
Author | SHA1 | Date | |
---|---|---|---|
|
c49dba2db0 | ||
|
133082ed58 | ||
|
cf4001d31c | ||
|
edd4f390ad |
7 changed files with 449 additions and 58 deletions
|
@ -45,7 +45,7 @@ import (
|
|||
|
||||
// Tuning parameters.
|
||||
const (
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
|
||||
defaultInitialGAS = 52000000_00000000
|
||||
defaultGCPeriod = 10000
|
||||
|
@ -2174,15 +2174,6 @@ func (bc *Blockchain) GetHeader(hash util.Uint256) (*block.Header, error) {
|
|||
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
|
||||
// block hash.
|
||||
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)
|
||||
}
|
||||
// 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 {
|
||||
case errors.Is(err, dao.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)
|
||||
}
|
||||
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())
|
||||
}
|
||||
case transaction.NotaryAssistedT:
|
||||
|
@ -2620,7 +2617,11 @@ func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *memp
|
|||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
} else if txpool.HasConflicts(t, bc) {
|
||||
|
|
|
@ -1634,28 +1634,159 @@ func TestBlockchain_VerifyTx(t *testing.T) {
|
|||
})
|
||||
t.Run("enabled", func(t *testing.T) {
|
||||
t.Run("dummy on-chain conflict", func(t *testing.T) {
|
||||
tx := newTestTx(t, h, testScript)
|
||||
require.NoError(t, accs[0].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(),
|
||||
t.Run("on-chain conflict signed by malicious party", func(t *testing.T) {
|
||||
tx := newTestTx(t, h, testScript)
|
||||
require.NoError(t, accs[0].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.NetworkFee = 1000_0000
|
||||
require.NoError(t, validator.SignTx(netmode.UnitTestNet, conflicting))
|
||||
e.AddNewBlock(t, conflicting)
|
||||
require.ErrorIs(t, bc.VerifyTx(tx), core.ErrHasConflicts)
|
||||
}
|
||||
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 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) {
|
||||
tx := neoValidatorsInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoOwner, neoOwner, 1, nil)
|
||||
|
|
|
@ -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
|
||||
// Transaction hash. It returns an error in case the transaction is in chain
|
||||
// or in the list of conflicting transactions.
|
||||
func (dao *Simple) HasTransaction(hash util.Uint256) error {
|
||||
// or in the list of conflicting transactions. If non-zero signers are specified,
|
||||
// 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)
|
||||
bytes, err := dao.Store.Get(key)
|
||||
if err != nil {
|
||||
|
@ -695,10 +698,33 @@ func (dao *Simple) HasTransaction(hash util.Uint256) error {
|
|||
if len(bytes) < 6 {
|
||||
return nil
|
||||
}
|
||||
if bytes[5] == transaction.DummyVersion {
|
||||
if bytes[5] != transaction.DummyVersion {
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
if len(signers) == 0 {
|
||||
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
|
||||
|
@ -805,17 +831,45 @@ func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32,
|
|||
}
|
||||
dao.Store.Put(key, buf.Bytes())
|
||||
if dao.Version.P2PSigExtensions {
|
||||
var value []byte
|
||||
var (
|
||||
valuePrefix []byte
|
||||
newSigners []byte
|
||||
)
|
||||
for _, attr := range tx.GetAttributes(transaction.ConflictsT) {
|
||||
hash := attr.Value.(*transaction.Conflicts).Hash
|
||||
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.WriteB(storage.ExecTransaction)
|
||||
buf.WriteU32LE(index)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,8 +186,8 @@ func TestStoreAsTransaction(t *testing.T) {
|
|||
}
|
||||
err := dao.StoreAsTransaction(tx, 0, aer)
|
||||
require.NoError(t, err)
|
||||
err = dao.HasTransaction(hash)
|
||||
require.NotNil(t, err)
|
||||
err = dao.HasTransaction(hash, nil)
|
||||
require.ErrorIs(t, err, ErrAlreadyExists)
|
||||
gotAppExecResult, err := dao.GetAppExecResults(hash, trigger.All)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(gotAppExecResult))
|
||||
|
@ -197,34 +197,84 @@ func TestStoreAsTransaction(t *testing.T) {
|
|||
t.Run("P2PSigExtensions on", func(t *testing.T) {
|
||||
dao := NewSimple(storage.NewMemoryStore(), false, true)
|
||||
conflictsH := util.Uint256{1, 2, 3}
|
||||
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 1)
|
||||
tx.Signers = append(tx.Signers, transaction.Signer{})
|
||||
tx.Scripts = append(tx.Scripts, transaction.Witness{})
|
||||
tx.Attributes = []transaction.Attribute{
|
||||
signer1 := util.Uint160{1, 2, 3}
|
||||
signer2 := util.Uint160{4, 5, 6}
|
||||
signer3 := util.Uint160{7, 8, 9}
|
||||
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,
|
||||
Value: &transaction.Conflicts{Hash: conflictsH},
|
||||
},
|
||||
}
|
||||
hash := tx.Hash()
|
||||
aer := &state.AppExecResult{
|
||||
Container: hash,
|
||||
hash1 := tx1.Hash()
|
||||
tx2 := transaction.New([]byte{byte(opcode.PUSH1)}, 1)
|
||||
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{
|
||||
Trigger: trigger.Application,
|
||||
Events: []state.NotificationEvent{},
|
||||
Stack: []stackitem.Item{},
|
||||
},
|
||||
}
|
||||
err := dao.StoreAsTransaction(tx, 0, aer)
|
||||
err := dao.StoreAsTransaction(tx1, 0, aer1)
|
||||
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)
|
||||
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)
|
||||
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.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])
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -542,8 +542,19 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !tx.HasSigner(existingTx.Signers[mp.payerIndex].Account) {
|
||||
return nil, fmt.Errorf("%w: not signed by the sender of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE())
|
||||
signers := make(map[util.Uint160]struct{}, len(existingTx.Signers))
|
||||
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
|
||||
conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx)
|
||||
|
|
|
@ -193,7 +193,7 @@ func (t *Transaction) decodeBinaryNoSize(br *io.BinReader, buf []byte) {
|
|||
br.Err = errors.New("too many witnesses")
|
||||
return
|
||||
} 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
|
||||
}
|
||||
t.Scripts = make([]Witness, nscripts)
|
||||
|
|
|
@ -3,6 +3,7 @@ package notary_test
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
@ -747,3 +748,146 @@ func TestNotary(t *testing.T) {
|
|||
}, 3*time.Second, 100*time.Millisecond)
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue