Merge pull request #3061 from nspcc-dev/check-onchain-conflicts

core: check the signers of on-chained conflicting transaction during new transaction verification
This commit is contained in:
Roman Khimov 2023-07-21 22:36:21 +03:00 committed by GitHub
commit 081f9d3ac5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 431 additions and 72 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,7 @@ 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 { if err := bc.dao.HasTransaction(t.Hash(), t.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 +2578,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 +2613,7 @@ 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 { if bc.dao.HasTransaction(t.Hash(), t.Signers) != nil {
return false return false
} }
} else if txpool.HasConflicts(t, bc) { } else if txpool.HasConflicts(t, bc) {

View file

@ -1634,28 +1634,272 @@ 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, `conflicting` 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 conflict 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 signer.
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, `conflicting` 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 [valid+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 conflict 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 fail verification, because one of the on-chained `conflicting`
// transactions has common signers with `tx`, thus, `conflicting` should be
// considered as a valid conflict.
require.ErrorIs(t, bc.VerifyTx(txGood), core.ErrHasConflicts)
})
t.Run("multiple on-chain conflicts signed by [malicious+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 conflict with anyone and signed by good signer.
txGood := newTestTx(t, good.ScriptHash(), testScript)
require.NoError(t, good.SignTx(netmode.UnitTestNet, txGood))
// 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)
// 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)
// We expect `tx` to fail verification, because one of the on-chained `conflicting`
// transactions has common signers with `tx`, thus, `conflicting` should be
// considered as a valid conflict.
require.ErrorIs(t, bc.VerifyTx(txGood), core.ErrHasConflicts)
})
t.Run("multiple on-chain conflicts signed by [valid + malicious + 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 conflict 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)
// txM3 conflicts with txGood and signed by one good and one malicious signers.
txM3 := newTestTx(t, good.ScriptHash(), testScript)
txM3.Signers = append(txM3.Signers, transaction.Signer{Account: m1.ScriptHash()})
txM3.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{
Hash: txGood.Hash(),
},
},
}
txM3.NetworkFee = 1_000_0000
require.NoError(t, good.SignTx(netmode.UnitTestNet, txM3))
require.NoError(t, m1.SignTx(netmode.UnitTestNet, txM3))
e.AddNewBlock(t, txM3)
// We expect `tx` to fail verification, because one of the on-chained `conflicting`
// transactions has common signers with `tx`, thus, `conflicting` should be
// considered as a valid conflict.
require.ErrorIs(t, bc.VerifyTx(txGood), core.ErrHasConflicts)
})
t.Run("on-chain conflict signed by single 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 []transaction.Signer) 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
sMap := make(map[util.Uint160]struct{}, len(signers))
for _, s := range signers {
sMap[s.Account] = struct{}{}
}
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)
}
if _, ok := sMap[u]; 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,18 +831,50 @@ 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 (
for _, attr := range tx.GetAttributes(transaction.ConflictsT) { valuePrefix []byte
newSigners []byte
)
attrs := tx.GetAttributes(transaction.ConflictsT)
for _, attr := range attrs {
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 {
buf.Reset() old, err := dao.Store.Get(key)
buf.WriteB(storage.ExecTransaction) if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
buf.WriteU32LE(index) return fmt.Errorf("failed to retrieve previous conflict record for %s: %w", hash.StringLE(), err)
buf.BinWriter.WriteB(transaction.DummyVersion) }
value = buf.Bytes() 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))
}
}
buf.Reset()
buf.WriteBytes(old)
if len(old) == 0 {
if len(valuePrefix) != 0 {
buf.WriteBytes(valuePrefix)
} else {
buf.WriteB(storage.ExecTransaction)
buf.WriteU32LE(index)
buf.WriteB(transaction.DummyVersion)
}
}
newSignersOffset := buf.Len()
if len(newSigners) == 0 {
for _, s := range tx.Signers {
s.Account.EncodeBinary(buf.BinWriter)
}
} else {
buf.WriteBytes(newSigners)
}
val := buf.Bytes()
dao.Store.Put(key, val)
if len(attrs) > 1 && len(valuePrefix) == 0 {
valuePrefix = slice.Copy(val[:6])
newSigners = slice.Copy(val[newSignersOffset:])
} }
dao.Store.Put(key, value)
} }
} }
return nil return nil

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, []transaction.Signer{{Account: signer1}})
require.ErrorIs(t, err, ErrHasConflicts)
// Conflicts: payer is important, conflict isn't malicious, test signer #2.
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer2}})
require.ErrorIs(t, err, ErrHasConflicts)
// Conflicts: payer is important, conflict isn't malicious, test signer #3.
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer3}})
require.ErrorIs(t, err, ErrHasConflicts)
// Conflicts: payer is important, conflict is malicious.
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: 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

@ -536,17 +536,31 @@ func (mp *Pool) checkTxConflicts(tx *transaction.Transaction, fee Feer) ([]*tran
} }
} }
// Step 2: check if mempooled transactions were in `tx`'s attributes. // Step 2: check if mempooled transactions were in `tx`'s attributes.
for _, attr := range tx.GetAttributes(transaction.ConflictsT) { conflictsAttrs := tx.GetAttributes(transaction.ConflictsT)
hash := attr.Value.(*transaction.Conflicts).Hash if len(conflictsAttrs) != 0 {
existingTx, ok := mp.verifiedMap[hash] txSigners := make(map[util.Uint160]struct{}, len(tx.Signers))
if !ok { for _, s := range tx.Signers {
continue txSigners[s.Account] = struct{}{}
} }
if !tx.HasSigner(existingTx.Signers[mp.payerIndex].Account) { for _, attr := range conflictsAttrs {
return nil, fmt.Errorf("%w: not signed by the sender of conflicting transaction %s", ErrConflictsAttribute, existingTx.Hash().StringBE()) hash := attr.Value.(*transaction.Conflicts).Hash
existingTx, ok := mp.verifiedMap[hash]
if !ok {
continue
}
var signerOK bool
for _, s := range existingTx.Signers {
if _, ok := txSigners[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)
} }
conflictingFee += existingTx.NetworkFee
conflictsToBeRemoved = append(conflictsToBeRemoved, existingTx)
} }
if conflictingFee != 0 && tx.NetworkFee <= conflictingFee { if conflictingFee != 0 && tx.NetworkFee <= conflictingFee {
return nil, fmt.Errorf("%w: conflicting transactions have bigger or equal network fee: %d vs %d", ErrConflictsAttribute, tx.NetworkFee, conflictingFee) return nil, fmt.Errorf("%w: conflicting transactions have bigger or equal network fee: %d vs %d", ErrConflictsAttribute, tx.NetworkFee, conflictingFee)

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), nscripts)
return return
} }
t.Scripts = make([]Witness, nscripts) t.Scripts = make([]Witness, nscripts)

@ -1 +1 @@
Subproject commit 02f2c68e7ba2694aff88c143631e7acf158d378a Subproject commit 7e5996844a90b514739f879bc9f873f9a34c9a67