forked from TrueCloudLab/neoneo-go
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 <shaleva.ann@nspcc.ru>
This commit is contained in:
parent
59c98c4d09
commit
58a086ea91
4 changed files with 127 additions and 1 deletions
|
@ -45,7 +45,7 @@ import (
|
||||||
|
|
||||||
// Tuning parameters.
|
// Tuning parameters.
|
||||||
const (
|
const (
|
||||||
version = "0.2.11"
|
version = "0.2.12"
|
||||||
|
|
||||||
// DefaultInitialGAS is the default amount of GAS emitted to the standby validators
|
// DefaultInitialGAS is the default amount of GAS emitted to the standby validators
|
||||||
// multisignature account during native GAS contract initialization.
|
// multisignature account during native GAS contract initialization.
|
||||||
|
|
|
@ -2494,3 +2494,42 @@ func TestNativenames(t *testing.T) {
|
||||||
require.Equal(t, cs.Manifest.Name, nativenames.All[i], i)
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -702,6 +702,12 @@ func (dao *Simple) HasTransaction(hash util.Uint256, signers []transaction.Signe
|
||||||
if len(bytes) < 5 { // (storage.ExecTransaction + index) for conflict record
|
if len(bytes) < 5 { // (storage.ExecTransaction + index) for conflict record
|
||||||
return nil
|
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 {
|
if len(bytes) != 5 {
|
||||||
return ErrAlreadyExists // fully-qualified transaction
|
return ErrAlreadyExists // fully-qualified transaction
|
||||||
}
|
}
|
||||||
|
@ -863,6 +869,17 @@ func (dao *Simple) StoreAsTransaction(tx *transaction.Transaction, index uint32,
|
||||||
// Conflict record stub.
|
// Conflict record stub.
|
||||||
hash := attr.Value.(*transaction.Conflicts).Hash
|
hash := attr.Value.(*transaction.Conflicts).Hash
|
||||||
copy(key[1:], hash.BytesBE())
|
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)
|
dao.Store.Put(key, val)
|
||||||
|
|
||||||
// Conflicting signers.
|
// Conflicting signers.
|
||||||
|
|
|
@ -242,6 +242,53 @@ func TestStoreAsTransaction(t *testing.T) {
|
||||||
}
|
}
|
||||||
err = dao.StoreAsTransaction(tx2, blockIndex, aer2)
|
err = dao.StoreAsTransaction(tx2, blockIndex, aer2)
|
||||||
require.NoError(t, err)
|
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)
|
err = dao.HasTransaction(hash1, nil, 0, 0)
|
||||||
require.ErrorIs(t, err, ErrAlreadyExists)
|
require.ErrorIs(t, err, ErrAlreadyExists)
|
||||||
err = dao.HasTransaction(hash2, nil, 0, 0)
|
err = dao.HasTransaction(hash2, nil, 0, 0)
|
||||||
|
@ -280,6 +327,29 @@ func TestStoreAsTransaction(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 1, len(gotAppExecResult))
|
require.Equal(t, 1, len(gotAppExecResult))
|
||||||
require.Equal(t, *aer2, gotAppExecResult[0])
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue