forked from TrueCloudLab/neoneo-go
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:
commit
081f9d3ac5
7 changed files with 431 additions and 72 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
2
pkg/vm/testdata/neo-vm
vendored
2
pkg/vm/testdata/neo-vm
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 02f2c68e7ba2694aff88c143631e7acf158d378a
|
Subproject commit 7e5996844a90b514739f879bc9f873f9a34c9a67
|
Loading…
Reference in a new issue