Skip to content

Commit 150bc86

Browse files
committed
core: improve conflict records storage scheme
Implement the neo-project/neo#2907 (comment) and port a part of neo-project/neo#2913. Signed-off-by: Anna Shaleva <[email protected]>
1 parent a0f9743 commit 150bc86

File tree

3 files changed

+79
-76
lines changed

3 files changed

+79
-76
lines changed

pkg/core/blockchain.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,7 +2498,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
24982498
return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
24992499
}
25002500
// check that current tx wasn't included in the conflicts attributes of some other transaction which is already in the chain
2501-
if err := bc.dao.HasTransaction(t.Hash(), t.Signers); err != nil {
2501+
if err := bc.dao.HasTransaction(t.Hash(), t.Signers, height, bc.config.MaxTraceableBlocks); err != nil {
25022502
switch {
25032503
case errors.Is(err, dao.ErrAlreadyExists):
25042504
return ErrAlreadyExists
@@ -2594,8 +2594,8 @@ func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transact
25942594
case transaction.ConflictsT:
25952595
conflicts := tx.Attributes[i].Value.(*transaction.Conflicts)
25962596
// Only fully-qualified dao.ErrAlreadyExists error bothers us here, thus, we
2597-
// can safely omit the payer argument to HasTransaction call to improve performance a bit.
2598-
if err := bc.dao.HasTransaction(conflicts.Hash, nil); errors.Is(err, dao.ErrAlreadyExists) {
2597+
// can safely omit the signers, current index and MTB arguments to HasTransaction call to improve performance a bit.
2598+
if err := bc.dao.HasTransaction(conflicts.Hash, nil, 0, 0); errors.Is(err, dao.ErrAlreadyExists) {
25992599
return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE())
26002600
}
26012601
case transaction.NotaryAssistedT:
@@ -2621,14 +2621,16 @@ func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transact
26212621
// was already done so we don't need to check basic things like size, input/output
26222622
// correctness, presence in blocks before the new one, etc.
26232623
func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool {
2624-
var recheckWitness bool
2625-
var curheight = bc.BlockHeight()
2624+
var (
2625+
recheckWitness bool
2626+
curheight = bc.BlockHeight()
2627+
)
26262628

26272629
if t.ValidUntilBlock <= curheight {
26282630
return false
26292631
}
26302632
if txpool == nil {
2631-
if bc.dao.HasTransaction(t.Hash(), t.Signers) != nil {
2633+
if bc.dao.HasTransaction(t.Hash(), t.Signers, curheight, bc.config.MaxTraceableBlocks) != nil {
26322634
return false
26332635
}
26342636
} else if txpool.HasConflicts(t, bc) {

pkg/core/dao/dao.go

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/nspcc-dev/neo-go/pkg/core/state"
1515
"github.com/nspcc-dev/neo-go/pkg/core/storage"
1616
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
17+
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
1718
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
1819
"github.com/nspcc-dev/neo-go/pkg/io"
1920
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
@@ -610,14 +611,15 @@ func (dao *Simple) GetTransaction(hash util.Uint256) (*transaction.Transaction,
610611
if err != nil {
611612
return nil, 0, err
612613
}
613-
if len(b) < 6 {
614+
if len(b) < 1 {
614615
return nil, 0, errors.New("bad transaction bytes")
615616
}
616617
if b[0] != storage.ExecTransaction {
617618
// It may be a block.
618619
return nil, 0, storage.ErrKeyNotFound
619620
}
620-
if b[5] == transaction.DummyVersion {
621+
if len(b) == 1+4 { // storage.ExecTransaction + index
622+
// It's a conflict record stub.
621623
return nil, 0, storage.ErrKeyNotFound
622624
}
623625
r := io.NewBinReaderFromBuf(b)
@@ -686,46 +688,48 @@ func (dao *Simple) StoreHeaderHashes(hashes []util.Uint256, height uint32) error
686688
// or in the list of conflicting transactions. If non-zero signers are specified,
687689
// then additional check against the conflicting transaction signers intersection
688690
// is held. Do not omit signers in case if it's important to check the validity
689-
// of a supposedly conflicting on-chain transaction.
690-
func (dao *Simple) HasTransaction(hash util.Uint256, signers []transaction.Signer) error {
691+
// of a supposedly conflicting on-chain transaction. The retrieved conflict isn't
692+
// checked against the maxTraceableBlocks setting if signers are omitted.
693+
// HasTransaction does not consider the case of block executable.
694+
func (dao *Simple) HasTransaction(hash util.Uint256, signers []transaction.Signer, currentIndex uint32, maxTraceableBlocks uint32) error {
691695
key := dao.makeExecutableKey(hash)
692696
bytes, err := dao.Store.Get(key)
693697
if err != nil {
694698
return nil
695699
}
696700

697-
if len(bytes) < 6 {
701+
if len(bytes) < 5 { // (storage.ExecTransaction + index) for conflict record
698702
return nil
699703
}
700-
if bytes[5] != transaction.DummyVersion {
701-
return ErrAlreadyExists
704+
if len(bytes) != 5 {
705+
return ErrAlreadyExists // fully-qualified transaction
702706
}
703707
if len(signers) == 0 {
704708
return ErrHasConflicts
705709
}
706710

707-
sMap := make(map[util.Uint160]struct{}, len(signers))
711+
if !isTraceableBlock(bytes[1:], currentIndex, maxTraceableBlocks) {
712+
// The most fresh conflict record is already outdated.
713+
return nil
714+
}
715+
708716
for _, s := range signers {
709-
sMap[s.Account] = struct{}{}
710-
}
711-
br := io.NewBinReaderFromBuf(bytes[6:])
712-
for {
713-
var u util.Uint160
714-
u.DecodeBinary(br)
715-
if br.Err != nil {
716-
if errors.Is(br.Err, iocore.EOF) {
717-
break
717+
v, err := dao.Store.Get(append(key, s.Account.BytesBE()...))
718+
if err == nil {
719+
if isTraceableBlock(v[1:], currentIndex, maxTraceableBlocks) {
720+
return ErrHasConflicts
718721
}
719-
return fmt.Errorf("failed to decode conflict record: %w", err)
720-
}
721-
if _, ok := sMap[u]; ok {
722-
return ErrHasConflicts
723722
}
724723
}
725724

726725
return nil
727726
}
728727

728+
func isTraceableBlock(indexBytes []byte, height, maxTraceableBlocks uint32) bool {
729+
index := binary.LittleEndian.Uint32(indexBytes[1:])
730+
return index <= height && index+maxTraceableBlocks > height
731+
}
732+
729733
// StoreAsBlock stores given block as DataBlock. It can reuse given buffer for
730734
// the purpose of value serialization.
731735
func (dao *Simple) StoreAsBlock(block *block.Block, aer1 *state.AppExecResult, aer2 *state.AppExecResult) error {
@@ -768,7 +772,30 @@ func (dao *Simple) DeleteBlock(h util.Uint256) error {
768772
for _, attr := range tx.GetAttributes(transaction.ConflictsT) {
769773
hash := attr.Value.(*transaction.Conflicts).Hash
770774
copy(key[1:], hash.BytesBE())
771-
dao.Store.Delete(key)
775+
776+
v, err := dao.Store.Get(key)
777+
if err != nil {
778+
return fmt.Errorf("failed to retrieve conflict record stub for %s (height %d, conflict %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), err)
779+
}
780+
index := binary.LittleEndian.Uint32(v[1:])
781+
// We can check for `<=` here, but use equality comparison to be more precise
782+
// and do not touch earlier conflict records (if any). Their removal must be triggered
783+
// by the caller code.
784+
if index == b.Index {
785+
dao.Store.Delete(key)
786+
}
787+
788+
for _, s := range tx.Signers {
789+
sKey := append(key, s.Account.BytesBE()...)
790+
v, err := dao.Store.Get(sKey)
791+
if err != nil {
792+
return fmt.Errorf("failed to retrieve conflict record for %s (height %d, conflict %s, signer %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), address.Uint160ToString(s.Account), err)
793+
}
794+
index = binary.LittleEndian.Uint32(v[1:])
795+
if index == b.Index {
796+
dao.Store.Delete(sKey)
797+
}
798+
}
772799
}
773800
}
774801

@@ -826,51 +853,20 @@ func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32,
826853
if buf.Err != nil {
827854
return buf.Err
828855
}
829-
dao.Store.Put(key, buf.Bytes())
856+
val := buf.Bytes()
857+
dao.Store.Put(key, val)
830858

831-
var (
832-
valuePrefix []byte
833-
newSigners []byte
834-
)
859+
val = val[:5] // storage.ExecTransaction (1 byte) + index (4 bytes)
835860
attrs := tx.GetAttributes(transaction.ConflictsT)
836861
for _, attr := range attrs {
862+
// Conflict record stub.
837863
hash := attr.Value.(*transaction.Conflicts).Hash
838864
copy(key[1:], hash.BytesBE())
839-
840-
old, err := dao.Store.Get(key)
841-
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
842-
return fmt.Errorf("failed to retrieve previous conflict record for %s: %w", hash.StringLE(), err)
843-
}
844-
if err == nil {
845-
if len(old) <= 6 { // storage.ExecTransaction + U32LE index + transaction.DummyVersion
846-
return fmt.Errorf("invalid conflict record format of length %d", len(old))
847-
}
848-
}
849-
buf.Reset()
850-
buf.WriteBytes(old)
851-
if len(old) == 0 {
852-
if len(valuePrefix) != 0 {
853-
buf.WriteBytes(valuePrefix)
854-
} else {
855-
buf.WriteB(storage.ExecTransaction)
856-
buf.WriteU32LE(index)
857-
buf.WriteB(transaction.DummyVersion)
858-
}
859-
}
860-
newSignersOffset := buf.Len()
861-
if len(newSigners) == 0 {
862-
for _, s := range tx.Signers {
863-
s.Account.EncodeBinary(buf.BinWriter)
864-
}
865-
} else {
866-
buf.WriteBytes(newSigners)
867-
}
868-
val := buf.Bytes()
869865
dao.Store.Put(key, val)
870866

871-
if len(attrs) > 1 && len(valuePrefix) == 0 {
872-
valuePrefix = slice.Copy(val[:6])
873-
newSigners = slice.Copy(val[newSignersOffset:])
867+
// Conflicting signers.
868+
for _, s := range tx.Signers {
869+
dao.Store.Put(append(key, s.Account.BytesBE()...), val)
874870
}
875871
}
876872
return nil

pkg/core/dao/dao_test.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func TestStoreAsTransaction(t *testing.T) {
186186
}
187187
err := dao.StoreAsTransaction(tx, 0, aer)
188188
require.NoError(t, err)
189-
err = dao.HasTransaction(hash, nil)
189+
err = dao.HasTransaction(hash, nil, 0, 0)
190190
require.ErrorIs(t, err, ErrAlreadyExists)
191191
gotAppExecResult, err := dao.GetAppExecResults(hash, trigger.All)
192192
require.NoError(t, err)
@@ -229,7 +229,8 @@ func TestStoreAsTransaction(t *testing.T) {
229229
Stack: []stackitem.Item{},
230230
},
231231
}
232-
err := dao.StoreAsTransaction(tx1, 0, aer1)
232+
const blockIndex = 5
233+
err := dao.StoreAsTransaction(tx1, blockIndex, aer1)
233234
require.NoError(t, err)
234235
aer2 := &state.AppExecResult{
235236
Container: hash2,
@@ -239,31 +240,35 @@ func TestStoreAsTransaction(t *testing.T) {
239240
Stack: []stackitem.Item{},
240241
},
241242
}
242-
err = dao.StoreAsTransaction(tx2, 0, aer2)
243+
err = dao.StoreAsTransaction(tx2, blockIndex, aer2)
243244
require.NoError(t, err)
244-
err = dao.HasTransaction(hash1, nil)
245+
err = dao.HasTransaction(hash1, nil, 0, 0)
245246
require.ErrorIs(t, err, ErrAlreadyExists)
246-
err = dao.HasTransaction(hash2, nil)
247+
err = dao.HasTransaction(hash2, nil, 0, 0)
247248
require.ErrorIs(t, err, ErrAlreadyExists)
248249

249250
// Conflicts: unimportant payer.
250-
err = dao.HasTransaction(conflictsH, nil)
251+
err = dao.HasTransaction(conflictsH, nil, 0, 0)
251252
require.ErrorIs(t, err, ErrHasConflicts)
252253

253254
// Conflicts: payer is important, conflict isn't malicious, test signer #1.
254-
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer1}})
255+
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer1}}, blockIndex+1, 5)
255256
require.ErrorIs(t, err, ErrHasConflicts)
256257

257258
// Conflicts: payer is important, conflict isn't malicious, test signer #2.
258-
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer2}})
259+
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer2}}, blockIndex+1, 5)
259260
require.ErrorIs(t, err, ErrHasConflicts)
260261

261262
// Conflicts: payer is important, conflict isn't malicious, test signer #3.
262-
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer3}})
263+
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer3}}, blockIndex+1, 5)
263264
require.ErrorIs(t, err, ErrHasConflicts)
264265

266+
// Conflicts: payer is important, conflict isn't malicious, but the conflict is far away than MTB.
267+
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signer3}}, blockIndex+10, 5)
268+
require.NoError(t, err)
269+
265270
// Conflicts: payer is important, conflict is malicious.
266-
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signerMalicious}})
271+
err = dao.HasTransaction(conflictsH, []transaction.Signer{{Account: signerMalicious}}, blockIndex+1, 5)
267272
require.NoError(t, err)
268273

269274
gotAppExecResult, err := dao.GetAppExecResults(hash1, trigger.All)

0 commit comments

Comments
 (0)