From 59c98c4d093b40222b26d30b01c3256f41af8c68 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 May 2024 14:03:20 +0300 Subject: [PATCH 1/5] core: always warn if accepted transaction fails verification These warnings must be monitored by developers since it might be a sign of behaviour difference between Go and C# nodes. Signed-off-by: Anna Shaleva --- pkg/core/blockchain.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3df35e004..ce27ddf3d 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1515,8 +1515,11 @@ func (bc *Blockchain) AddBlock(block *block.Block) error { } else { err = bc.verifyAndPoolTx(tx, mp, bc) } - if err != nil && bc.config.VerifyTransactions { - return fmt.Errorf("transaction %s failed to verify: %w", tx.Hash().StringLE(), err) + if err != nil { + if bc.config.VerifyTransactions { + return fmt.Errorf("transaction %s failed to verify: %w", tx.Hash().StringLE(), err) + } + bc.log.Warn(fmt.Sprintf("transaction %s failed to verify: %s", tx.Hash().StringLE(), err)) } } } From 58a086ea91ddc4a97062911a3c2bcbd6e004d2c6 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 May 2024 14:06:38 +0300 Subject: [PATCH 2/5] core: allow transaction to conflict with block Transaction 0x289c235dcdab8be7426d05f0fbb5e86c619f81481ea136493fa95deee5dbb7cc is already on mainnet at block 5272006 and we can't do anything with it. This transaction has genesis block hash in Conflicts attribute. It leads to the following consequences: 1. Genesis block executable record is overwritten by conflict record stub. Genesis block can't be retrieved anymore. This bug is described in #3427. 2. Somehow this transaction has passed verification on NeoGo CN without any warnings: ``` Apr 24 16:12:30 kangra neo-go[2453907]: 2024-04-24T16:12:30.865+0300 INFO initializing dbft {"height": 5272006, "view": 0, "index": 6, "role": "Backup"} Apr 24 16:12:31 kangra neo-go[2453907]: 2024-04-24T16:12:31.245+0300 INFO persisted to disk {"blocks": 1, "keys": 37, "headerHeight": 5272005, "blockHeight": 5272005, "took": "14.548903ms"} Apr 24 16:12:34 kangra neo-go[2453907]: 2024-04-24T16:12:34.977+0300 ERROR can't add SV-signed state root {"error": "stateroot mismatch at block 5272005: 9d5f95784f26c862d6f889f213aad1e3330611880c02330e88db8802c750aa46 vs d25304d518645df725014897d13bbf023919928e79074abcea48f31cf9f32a25"} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.820+0300 INFO received PrepareRequest {"validator": 5, "tx": 1} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.821+0300 INFO sending PrepareResponse {"height": 5272006, "view": 0} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.827+0300 INFO received PrepareResponse {"validator": 4} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.830+0300 INFO received PrepareResponse {"validator": 3} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.875+0300 INFO received PrepareResponse {"validator": 2} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.878+0300 INFO sending Commit {"height": 5272006, "view": 0} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.879+0300 INFO received Commit {"validator": 4} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.881+0300 INFO received PrepareResponse {"validator": 0} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.881+0300 INFO received Commit {"validator": 3} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.906+0300 INFO received Commit {"validator": 0} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.907+0300 INFO received PrepareResponse {"validator": 1} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.915+0300 INFO received Commit {"validator": 1} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.915+0300 INFO approving block {"height": 5272006, "hash": "6b111519537343ce579d04ccad71c43318b12c680d0f374dfcd466aa22643fb6", "tx_count": 1, "merkle": "ccb7dbe5ee5da93f4936a11e48819f616ce8b5fbf0056d42e78babcd5d239c28", "prev": "12ad6cc5d0cd357b9fc9fb0c1a016ba8014d3cdd5a96818598e6a40a1a4a2a21"} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.917+0300 WARN contract invocation failed {"tx": "289c235dcdab8be7426d05f0fbb5e86c619f81481ea136493fa95deee5dbb7cc", "block": 5272006, "error": "at instruction 86 (ASSERT): ASSERT failed"} Apr 24 16:12:45 kangra neo-go[2453907]: 2024-04-24T16:12:45.950+0300 INFO initializing dbft {"height": 5272007, "view": 0, "index": 6, "role": "Primary"} Apr 24 16:12:46 kangra neo-go[2453907]: 2024-04-24T16:12:46.256+0300 INFO persisted to disk {"blocks": 1, "keys": 67, "headerHeight": 5272006, "blockHeight": 5272006, "took": "16.576594ms"} ``` And thus, we must treat this transaction as valid for this behaviour to be reproducable. This commit contains two fixes: 1. Do not overwrite block executable records by conflict record stubs. If some transaction conflicts with block, then just skip the conflict record stub for this attribute since it's impossible to create transaction with the same hash. 2. Do not fail verification for those transactions that have Conflicts attribute with block hash inside. This one is controversial, but we have to adjust this code to treat already accepted transaction as valid. Close #3427. The transaction itself: ``` { "id" : 1, "jsonrpc" : "2.0", "result" : { "attributes" : [ { "height" : 0, "type" : "NotValidBefore" }, { "hash" : "0x1f4d1defa46faa5e7b9b8d3f79a06bec777d7c26c4aa5f6f5899a291daa87c15", "type" : "Conflicts" } ], "blockhash" : "0xb63f6422aa66d4fc4d370f0d682cb11833c471adcc049d57ce4373531915116b", "blocktime" : 1713964365700, "confirmations" : 108335, "hash" : "0x289c235dcdab8be7426d05f0fbb5e86c619f81481ea136493fa95deee5dbb7cc", "netfee" : "237904", "nonce" : 0, "script" : "CxAMFIPvkoyXujYCRmgq9qEfMJQ4wNveDBSD75KMl7o2AkZoKvahHzCUOMDb3hTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1I5", "sender" : "NbcGB1tBEGM5MfhNbDAimvpJKzvVjLQ3jW", "signers" : [ { "account" : "0x649ca095e38a790d6c15ff78e0c6175099b428ac", "scopes" : "None" }, { "account" : "0xdedbc03894301fa1f62a68460236ba978c92ef83", "scopes" : "None" } ], "size" : 412, "sysfee" : "997778", "validuntilblock" : 5277629, "version" : 0, "vmstate" : "FAULT", "witnesses" : [ { "invocation" : "DECw8XNuyRg5vPeHxisQXlZ7VYNDxxK4xEm8zwpPyWJSSu+JaRKQxdrlPkXxXj34wc4ZSrZvKICGgPFE0ZHXhLPo", "verification" : "DCEC+PI2tRSlp0wGwnjRuQdWdI0tBXNS7SlzSBBHFsaKUsdBVuezJw==" }, { "invocation" : "DEAxwi97t+rg9RsccOUzdJTJK7idbR7uUqQp0/0/ob9FbuW/tFius3/FOi82PDZtwdhk7s7KiNM/pU7vZLsgIbM0", "verification" : "DCEDbInkzF5llzmgljE4HSMvtrNgPaz73XO5wgVJXLHNLXRBVuezJw==" } ] } } ``` Signed-off-by: Anna Shaleva --- pkg/core/blockchain.go | 2 +- pkg/core/blockchain_neotest_test.go | 39 ++++++++++++++++ pkg/core/dao/dao.go | 17 +++++++ pkg/core/dao/dao_test.go | 70 +++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index ce27ddf3d..308aa3be7 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -45,7 +45,7 @@ import ( // Tuning parameters. const ( - version = "0.2.11" + version = "0.2.12" // DefaultInitialGAS is the default amount of GAS emitted to the standby validators // multisignature account during native GAS contract initialization. diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go index bec3fa91e..e40cc802d 100644 --- a/pkg/core/blockchain_neotest_test.go +++ b/pkg/core/blockchain_neotest_test.go @@ -2494,3 +2494,42 @@ func TestNativenames(t *testing.T) { require.Equal(t, cs.Manifest.Name, nativenames.All[i], i) } } + +// TestBlockchain_StoreAsTransaction_ExecutableConflict ensures that transaction conflicting with +// some on-chain block can be properly stored and doesn't break the database. +func TestBlockchain_StoreAsTransaction_ExecutableConflict(t *testing.T) { + bc, acc := chain.NewSingleWithCustomConfig(t, nil) + e := neotest.NewExecutor(t, bc, acc, acc) + genesisH := bc.GetHeaderHash(0) + currHeight := bc.BlockHeight() + + // Ensure AER can be retrieved for genesis block. + aer, err := bc.GetAppExecResults(genesisH, trigger.All) + require.NoError(t, err) + require.Equal(t, 2, len(aer)) + + tx := transaction.New([]byte{byte(opcode.PUSHT)}, 0) + tx.Nonce = 5 + tx.ValidUntilBlock = e.Chain.BlockHeight() + 1 + tx.Attributes = []transaction.Attribute{{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: genesisH}}} + e.SignTx(t, tx, -1, acc) + e.AddNewBlock(t, tx) + e.CheckHalt(t, tx.Hash(), stackitem.Make(true)) + + // Ensure original tx can be retrieved. + actual, actualHeight, err := bc.GetTransaction(tx.Hash()) + require.NoError(t, err) + require.Equal(t, currHeight+1, actualHeight) + require.Equal(t, tx, actual, tx) + + // Ensure conflict stub is not stored. This check doesn't give us 100% sure that + // there's no specific conflict record since GetTransaction doesn't return conflict records, + // but at least it allows to ensure that no transaction record is present. + _, _, err = bc.GetTransaction(genesisH) + require.ErrorIs(t, err, storage.ErrKeyNotFound) + + // Ensure AER still can be retrieved for genesis block. + aer, err = bc.GetAppExecResults(genesisH, trigger.All) + require.NoError(t, err) + require.Equal(t, 2, len(aer)) +} diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 218be9123..961579a1c 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -702,6 +702,12 @@ func (dao *Simple) HasTransaction(hash util.Uint256, signers []transaction.Signe if len(bytes) < 5 { // (storage.ExecTransaction + index) for conflict record return nil } + if bytes[0] != storage.ExecTransaction { + // It's a block, thus no conflict. This path is needed since there's a transaction accepted on mainnet + // that conflicts with block. This transaction was declined by Go nodes, but accepted by C# nodes, and hence + // we need to adjust Go behaviour post-factum. Ref. #3427 and 0x289c235dcdab8be7426d05f0fbb5e86c619f81481ea136493fa95deee5dbb7cc. + return nil + } if len(bytes) != 5 { return ErrAlreadyExists // fully-qualified transaction } @@ -863,6 +869,17 @@ func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32, // Conflict record stub. hash := attr.Value.(*transaction.Conflicts).Hash copy(key[1:], hash.BytesBE()) + + // A short path if there's a block with the matching hash. If it's there, then + // don't store the conflict record stub and conflict signers since it's a + // useless record, no transaction with the same hash is possible. + exec, err := dao.Store.Get(key) + if err == nil { + if len(exec) > 0 && exec[0] != storage.ExecTransaction { + continue + } + } + dao.Store.Put(key, val) // Conflicting signers. diff --git a/pkg/core/dao/dao_test.go b/pkg/core/dao/dao_test.go index 7149cdd17..9db364118 100644 --- a/pkg/core/dao/dao_test.go +++ b/pkg/core/dao/dao_test.go @@ -242,6 +242,53 @@ func TestStoreAsTransaction(t *testing.T) { } err = dao.StoreAsTransaction(tx2, blockIndex, aer2) require.NoError(t, err) + + // A special transaction that conflicts with genesis block. + genesis := &block.Block{ + Header: block.Header{ + Version: 0, + Timestamp: 123, + Nonce: 1, + Index: 0, + NextConsensus: util.Uint160{1, 2, 3}, + }, + } + genesisAer1 := &state.AppExecResult{ + Container: genesis.Hash(), + Execution: state.Execution{ + Trigger: trigger.OnPersist, + Events: []state.NotificationEvent{}, + Stack: []stackitem.Item{}, + }, + } + genesisAer2 := &state.AppExecResult{ + Container: genesis.Hash(), + Execution: state.Execution{ + Trigger: trigger.PostPersist, + Events: []state.NotificationEvent{}, + Stack: []stackitem.Item{}, + }, + } + require.NoError(t, dao.StoreAsBlock(genesis, genesisAer1, genesisAer2)) + tx3 := transaction.New([]byte{byte(opcode.PUSH1)}, 1) + tx3.Signers = append(tx3.Signers, transaction.Signer{Account: signer1}) + tx3.Scripts = append(tx3.Scripts, transaction.Witness{}) + tx3.Attributes = []transaction.Attribute{ + { + Type: transaction.ConflictsT, + Value: &transaction.Conflicts{Hash: genesis.Hash()}, + }, + } + hash3 := tx3.Hash() + aer3 := &state.AppExecResult{ + Container: hash3, + Execution: state.Execution{ + Trigger: trigger.Application, + Events: []state.NotificationEvent{}, + Stack: []stackitem.Item{}, + }, + } + err = dao.HasTransaction(hash1, nil, 0, 0) require.ErrorIs(t, err, ErrAlreadyExists) err = dao.HasTransaction(hash2, nil, 0, 0) @@ -280,6 +327,29 @@ func TestStoreAsTransaction(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(gotAppExecResult)) require.Equal(t, *aer2, gotAppExecResult[0]) + + // Ensure block is not treated as transaction. + err = dao.HasTransaction(genesis.Hash(), nil, 0, 0) + require.NoError(t, err) + + // Store tx3 and ensure genesis executable record is not corrupted. + require.NoError(t, dao.StoreAsTransaction(tx3, 0, aer3)) + err = dao.HasTransaction(hash3, nil, 0, 0) + require.ErrorIs(t, err, ErrAlreadyExists) + actualAer, err := dao.GetAppExecResults(hash3, trigger.All) + require.NoError(t, err) + require.Equal(t, 1, len(actualAer)) + require.Equal(t, *aer3, actualAer[0]) + actualGenesisAer, err := dao.GetAppExecResults(genesis.Hash(), trigger.All) + require.NoError(t, err) + require.Equal(t, 2, len(actualGenesisAer)) + require.Equal(t, *genesisAer1, actualGenesisAer[0]) + require.Equal(t, *genesisAer2, actualGenesisAer[1]) + + // A special requirement for transactions that conflict with block: they should + // not produce conflict record stub, ref. #3427. + err = dao.HasTransaction(genesis.Hash(), nil, 0, 0) + require.NoError(t, err) }) } From 9a1075d332e21963d7b11d9895f2a7d2b27782b7 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 May 2024 14:29:27 +0300 Subject: [PATCH 3/5] dao: fix transaction application log decoding Conflict record stub has value of 5 bytes length: 1 byte for storage.ExecTransaction prefix and 4 bytes for the block index LE. This scheme was implemented in #3138, and this commit should be a part of this PR. Also, transaction.DummyVersion is removed since it's unused anymore. Close #3426. The reason of `failed to locate application log: EOF` error during genesis AER request is in the following: genesis executable was overwritten by conflict record stub produced by transaction 0x289c235dcdab8be7426d05f0fbb5e86c619f81481ea136493fa95deee5dbb7cc (ref. #3427). As a consequence, an attempt to decode transaction AER was initited, but conflict record scheme was changed in #3138. Signed-off-by: Anna Shaleva --- pkg/core/dao/dao.go | 9 +++++---- pkg/core/transaction/transaction.go | 4 +--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 961579a1c..4adeec4e7 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -323,7 +323,7 @@ func (dao *Simple) GetTxExecResult(hash util.Uint256) (uint32, *transaction.Tran // decodeTxAndExecResult decodes transaction, its height and execution result from // the given executable bytes. It performs no executable prefix check. func decodeTxAndExecResult(buf []byte) (uint32, *transaction.Transaction, *state.AppExecResult, error) { - if len(buf) >= 6 && buf[5] == transaction.DummyVersion { + if len(buf) == 1+4 { // conflict record stub. return 0, nil, nil, storage.ErrKeyNotFound } r := io.NewBinReaderFromBuf(buf) @@ -605,7 +605,7 @@ func (dao *Simple) DeleteHeaderHashes(since uint32, batchSize int) { } // GetTransaction returns Transaction and its height by the given hash -// if it exists in the store. It does not return dummy transactions. +// if it exists in the store. It does not return conflict record stubs. func (dao *Simple) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) { key := dao.makeExecutableKey(hash) b, err := dao.Store.Get(key) @@ -844,8 +844,9 @@ func (dao *Simple) StoreAsCurrentBlock(block *block.Block) { dao.Store.Put(dao.mkKeyPrefix(storage.SYSCurrentBlock), buf.Bytes()) } -// StoreAsTransaction stores the given TX as DataTransaction. It also stores transactions -// the given tx has conflicts with as DataTransaction with dummy version. It can reuse the given +// StoreAsTransaction stores the given TX as DataTransaction. It also stores conflict records +// (hashes of transactions the given tx has conflicts with) as DataTransaction with value containing +// only five bytes: 1-byte [storage.ExecTransaction] executable prefix + 4-bytes-LE block index. It can reuse the given // buffer for the purpose of value serialization. func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32, aer *state.AppExecResult) error { key := dao.makeExecutableKey(tx.Hash()) diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index a41c811a4..443e75efa 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -26,8 +26,6 @@ const ( // MaxAttributes is maximum number of attributes including signers that can be contained // within a transaction. It is set to be 16. MaxAttributes = 16 - // DummyVersion represents reserved transaction version for trimmed transactions. - DummyVersion = 255 ) // ErrInvalidWitnessNum returns when the number of witnesses does not match signers. @@ -408,7 +406,7 @@ var ( // isValid checks whether decoded/unmarshalled transaction has all fields valid. func (t *Transaction) isValid() error { - if t.Version > 0 && t.Version != DummyVersion { + if t.Version > 0 { return ErrInvalidVersion } if t.SystemFee < 0 { From 2ad4c86712444edd03081a2deb5fc38b100467e5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 May 2024 14:43:06 +0300 Subject: [PATCH 4/5] dao: move conflict record value length to a separate const Conflicts-related code contains more and more these magic numbers, and there's no good in it even if all the usages are commented. This approach produces bugs like #3426. No functional changes, just a refactoring. Signed-off-by: Anna Shaleva --- pkg/core/dao/dao.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 4adeec4e7..ba5713d93 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -35,6 +35,11 @@ var ( ErrInternalDBInconsistency = errors.New("internal DB inconsistency") ) +// conflictRecordValueLen is the length of value of transaction conflict record. +// It consists of 1-byte [storage.ExecTransaction] prefix and 4-bytes block index +// in the LE form. +const conflictRecordValueLen = 1 + 4 + // Simple is memCached wrapper around DB, simple DAO implementation. type Simple struct { Version Version @@ -323,7 +328,7 @@ func (dao *Simple) GetTxExecResult(hash util.Uint256) (uint32, *transaction.Tran // decodeTxAndExecResult decodes transaction, its height and execution result from // the given executable bytes. It performs no executable prefix check. func decodeTxAndExecResult(buf []byte) (uint32, *transaction.Transaction, *state.AppExecResult, error) { - if len(buf) == 1+4 { // conflict record stub. + if len(buf) == conflictRecordValueLen { // conflict record stub. return 0, nil, nil, storage.ErrKeyNotFound } r := io.NewBinReaderFromBuf(buf) @@ -619,7 +624,7 @@ func (dao *Simple) GetTransaction(hash util.Uint256) (*transaction.Transaction, // It may be a block. return nil, 0, storage.ErrKeyNotFound } - if len(b) == 1+4 { // storage.ExecTransaction + index + if len(b) == conflictRecordValueLen { // It's a conflict record stub. return nil, 0, storage.ErrKeyNotFound } @@ -699,7 +704,7 @@ func (dao *Simple) HasTransaction(hash util.Uint256, signers []transaction.Signe return nil } - if len(bytes) < 5 { // (storage.ExecTransaction + index) for conflict record + if len(bytes) < conflictRecordValueLen { // (storage.ExecTransaction + index) for conflict record return nil } if bytes[0] != storage.ExecTransaction { @@ -708,7 +713,7 @@ func (dao *Simple) HasTransaction(hash util.Uint256, signers []transaction.Signe // we need to adjust Go behaviour post-factum. Ref. #3427 and 0x289c235dcdab8be7426d05f0fbb5e86c619f81481ea136493fa95deee5dbb7cc. return nil } - if len(bytes) != 5 { + if len(bytes) != conflictRecordValueLen { return ErrAlreadyExists // fully-qualified transaction } if len(signers) == 0 { @@ -864,7 +869,7 @@ func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32, val := buf.Bytes() dao.Store.Put(key, val) - val = val[:5] // storage.ExecTransaction (1 byte) + index (4 bytes) + val = val[:conflictRecordValueLen] // storage.ExecTransaction (1 byte) + index (4 bytes) attrs := tx.GetAttributes(transaction.ConflictsT) for _, attr := range attrs { // Conflict record stub. From dc8b2f639a144b898c7684898c77afb01d25479a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 May 2024 14:46:49 +0300 Subject: [PATCH 5/5] dao: do not remove block executable by conflict record stub It's possible for transaction to include block hash into Conflicts attribure. If so, then we must not remove block executable record while cleaning transation's conflict records. This commit is a direct consequence of e6ceee0f230a21c87006a9297636be29c0d8ea47. Ref. #3427. Signed-off-by: Anna Shaleva --- pkg/core/dao/dao.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index ba5713d93..579d35548 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -789,6 +789,10 @@ func (dao *Simple) DeleteBlock(h util.Uint256) error { if err != nil { return fmt.Errorf("failed to retrieve conflict record stub for %s (height %d, conflict %s): %w", tx.Hash().StringLE(), b.Index, hash.StringLE(), err) } + // It might be a block since we allow transactions to have block hash in the Conflicts attribute. + if v[0] != storage.ExecTransaction { + continue + } index := binary.LittleEndian.Uint32(v[1:]) // We can check for `<=` here, but use equality comparison to be more precise // and do not touch earlier conflict records (if any). Their removal must be triggered