Skip to content

Commit 8d270e6

Browse files
AnnaShalevashargonJim8y
authored
Improved scheme for Conflicts attribute storing and pricing (#2913)
* Ledger: change conflict records storage scheme This commit is a part of #2907: it implements conflict records storage scheme discribed in #2907 (comment). The short scheme description: Do not store the list of conflicting signers in the Ledger's conflict record value. Instead, put each conflicting signer in the conflict record key so that the reculting key is: {Prefix_Transaction, <conflict hash>, <signer>}, and the value is {<block index>}. As an optimisation, for each conflict record store the dummy stub where the key is {Prefix_Transaction, <conflict hash>} and the value is {<block index>}. This optimisation allows to reduce DB read requests during newly-pooled transaction verification for those transactions that do not have any on-chained conflicts. Also, IsTraceableBlock check is added for on-chain conflicts verification. It is needed to avoid situations when untraceable on-chained conflict affects the newly-pooled transaction verification. Signed-off-by: Anna Shaleva <[email protected]> * UT_MemoryPool: remove unused test Malicious_OnChain_Conflict was constructed incorrectly (see the comment in the end of the test) and was replaced by the proper TestMaliciousOnChainConflict test in UT_Blockchain.cs way back ago in 0c06c91. Signed-off-by: Anna Shaleva <[email protected]> * Policy: introduce fee for Conflicts attribute This commit implements Conflicts attribute policy described in #2907 (comment). Signed-off-by: Anna Shaleva <[email protected]> * *: remove remnants of ConflictsFee in native Policy ConflictsFee logic was replaced by the generic attribute fee mechanism implemented in #2916. Signed-off-by: Anna Shaleva <[email protected]> * Native: do not remove malicious conflict records during OnPersist It's OK to keep them and save O(n) operations during OnPersist. The storage taken by these malicious conflict records is properly paid anyway. See the discussion under #2913 (comment). Signed-off-by: Anna Shaleva <[email protected]> * Properly rewrite previously added malicious conflict if it's in the storage `engine.Snapshot.Add` doesn't allow to rewrite storage entity if it's already exist. Thus, we firstly need to remove it and afterwards to add the updated entity. Signed-off-by: Anna Shaleva <[email protected]> * Throw proper exception if TestMaliciousOnChainConflict fails Signed-off-by: Anna Shaleva <[email protected]> * Optimize conflicts records storing Use Snapshot.GetAndChange instead of subsequent calls to Delete and Add. Ref. #2913 (comment). Signed-off-by: Anna Shaleva <[email protected]> --------- Signed-off-by: Anna Shaleva <[email protected]> Co-authored-by: Shargon <[email protected]> Co-authored-by: Jimmy <[email protected]>
1 parent ec6ddf6 commit 8d270e6

File tree

7 files changed

+55
-99
lines changed

7 files changed

+55
-99
lines changed

src/Neo/Ledger/Blockchain.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ private void OnFillMemoryPool(IEnumerable<Transaction> transactions)
206206
{
207207
if (NativeContract.Ledger.ContainsTransaction(snapshot, tx.Hash))
208208
continue;
209-
if (NativeContract.Ledger.ContainsConflictHash(snapshot, tx.Hash, tx.Signers.Select(s => s.Account)))
209+
if (NativeContract.Ledger.ContainsConflictHash(snapshot, tx.Hash, tx.Signers.Select(s => s.Account), system.Settings.MaxTraceableBlocks))
210210
continue;
211211
// First remove the tx if it is unverified in the pool.
212212
system.MemPool.TryRemoveUnVerified(tx.Hash, out _);

src/Neo/NeoSystem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ public bool ContainsTransaction(UInt256 hash)
287287
/// <returns><see langword="true"/> if the transaction conflicts with on-chain transaction; otherwise, <see langword="false"/>.</returns>
288288
public bool ContainsConflictHash(UInt256 hash, IEnumerable<UInt160> signers)
289289
{
290-
return NativeContract.Ledger.ContainsConflictHash(StoreView, hash, signers);
290+
return NativeContract.Ledger.ContainsConflictHash(StoreView, hash, signers, Settings.MaxTraceableBlocks);
291291
}
292292
}
293293
}

src/Neo/SmartContract/Native/LedgerContract.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,19 @@ internal override ContractTask OnPersist(ApplicationEngine engine)
4747
engine.Snapshot.Add(CreateStorageKey(Prefix_Block).Add(engine.PersistingBlock.Hash), new StorageItem(Trim(engine.PersistingBlock).ToArray()));
4848
foreach (TransactionState tx in transactions)
4949
{
50-
engine.Snapshot.Add(CreateStorageKey(Prefix_Transaction).Add(tx.Transaction.Hash), new StorageItem(tx));
50+
// It's possible that there are previously saved malicious conflict records for this transaction.
51+
// If so, then remove it and store the relevant transaction itself.
52+
engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(tx.Transaction.Hash), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(tx));
53+
54+
// Store transaction's conflicits.
5155
var conflictingSigners = tx.Transaction.Signers.Select(s => s.Account);
5256
foreach (var attr in tx.Transaction.GetAttributes<Conflicts>())
5357
{
54-
var conflictRecord = engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(attr.Hash),
55-
() => new StorageItem(new TransactionState { ConflictingSigners = Array.Empty<UInt160>() })).GetInteroperable<TransactionState>();
56-
conflictRecord.ConflictingSigners = conflictRecord.ConflictingSigners.Concat(conflictingSigners).Distinct().ToArray();
58+
engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(attr.Hash), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index }));
59+
foreach (var signer in conflictingSigners)
60+
{
61+
engine.Snapshot.GetAndChange(CreateStorageKey(Prefix_Transaction).Add(attr.Hash).Add(signer), () => new StorageItem(new TransactionState())).FromReplica(new StorageItem(new TransactionState() { BlockIndex = engine.PersistingBlock.Index }));
62+
}
5763
}
5864
}
5965
engine.SetState(transactions);
@@ -145,11 +151,24 @@ public bool ContainsTransaction(DataCache snapshot, UInt256 hash)
145151
/// <param name="snapshot">The snapshot used to read data.</param>
146152
/// <param name="hash">The hash of the conflicting transaction.</param>
147153
/// <param name="signers">The list of signer accounts of the conflicting transaction.</param>
154+
/// <param name="maxTraceableBlocks">MaxTraceableBlocks protocol setting.</param>
148155
/// <returns><see langword="true"/> if the blockchain contains the hash of the conflicting transaction; otherwise, <see langword="false"/>.</returns>
149-
public bool ContainsConflictHash(DataCache snapshot, UInt256 hash, IEnumerable<UInt160> signers)
156+
public bool ContainsConflictHash(DataCache snapshot, UInt256 hash, IEnumerable<UInt160> signers, uint maxTraceableBlocks)
150157
{
151-
var state = snapshot.TryGet(CreateStorageKey(Prefix_Transaction).Add(hash))?.GetInteroperable<TransactionState>();
152-
return state is not null && state.Transaction is null && (signers is null || state.ConflictingSigners.Intersect(signers).Any());
158+
// Check the dummy stub firstly to define whether there's exist at least one conflict record.
159+
var stub = snapshot.TryGet(CreateStorageKey(Prefix_Transaction).Add(hash))?.GetInteroperable<TransactionState>();
160+
if (stub is null || stub.Transaction is not null || !IsTraceableBlock(snapshot, stub.BlockIndex, maxTraceableBlocks))
161+
return false;
162+
163+
// At least one conflict record is found, then need to check signers intersection.
164+
foreach (var signer in signers)
165+
{
166+
var state = snapshot.TryGet(CreateStorageKey(Prefix_Transaction).Add(hash).Add(signer))?.GetInteroperable<TransactionState>();
167+
if (state is not null && IsTraceableBlock(snapshot, state.BlockIndex, maxTraceableBlocks))
168+
return true;
169+
}
170+
171+
return false;
153172
}
154173

155174
/// <summary>

src/Neo/SmartContract/Native/TransactionState.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ public class TransactionState : IInteroperable
3232
/// </summary>
3333
public Transaction Transaction;
3434

35-
public UInt160[] ConflictingSigners;
36-
3735
/// <summary>
3836
/// The execution state
3937
/// </summary>
@@ -47,7 +45,6 @@ IInteroperable IInteroperable.Clone()
4745
{
4846
BlockIndex = BlockIndex,
4947
Transaction = Transaction,
50-
ConflictingSigners = ConflictingSigners,
5148
State = State,
5249
_rawTransaction = _rawTransaction
5350
};
@@ -58,7 +55,6 @@ void IInteroperable.FromReplica(IInteroperable replica)
5855
TransactionState from = (TransactionState)replica;
5956
BlockIndex = from.BlockIndex;
6057
Transaction = from.Transaction;
61-
ConflictingSigners = from.ConflictingSigners;
6258
State = from.State;
6359
if (_rawTransaction.IsEmpty)
6460
_rawTransaction = from._rawTransaction;
@@ -67,20 +63,21 @@ void IInteroperable.FromReplica(IInteroperable replica)
6763
void IInteroperable.FromStackItem(StackItem stackItem)
6864
{
6965
Struct @struct = (Struct)stackItem;
70-
if (@struct.Count == 1)
71-
{
72-
ConflictingSigners = ((VM.Types.Array)@struct[0]).Select(u => new UInt160(u.GetSpan())).ToArray();
73-
return;
74-
}
7566
BlockIndex = (uint)@struct[0].GetInteger();
67+
68+
// Conflict record.
69+
if (@struct.Count == 1) return;
70+
71+
// Fully-qualified transaction.
7672
_rawTransaction = ((ByteString)@struct[1]).Memory;
7773
Transaction = _rawTransaction.AsSerializable<Transaction>();
7874
State = (VMState)(byte)@struct[2].GetInteger();
7975
}
8076

8177
StackItem IInteroperable.ToStackItem(ReferenceCounter referenceCounter)
8278
{
83-
if (Transaction is null) return new Struct(referenceCounter) { new VM.Types.Array(referenceCounter, ConflictingSigners.Select(u => new ByteString(u.ToArray())).ToArray()) };
79+
if (Transaction is null)
80+
return new Struct(referenceCounter) { BlockIndex };
8481
if (_rawTransaction.IsEmpty)
8582
_rawTransaction = Transaction.ToArray();
8683
return new Struct(referenceCounter) { BlockIndex, _rawTransaction, (byte)State };

tests/Neo.UnitTests/Ledger/UT_Blockchain.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public void TestMaliciousOnChainConflict()
135135
{
136136
Header = new Header()
137137
{
138-
Index = 10000,
138+
Index = 5, // allow tx1, tx2 and tx3 to fit into MaxValidUntilBlockIncrement.
139139
MerkleRoot = UInt256.Zero,
140140
NextConsensus = UInt160.Zero,
141141
PrevHash = UInt256.Zero,
@@ -149,13 +149,26 @@ public void TestMaliciousOnChainConflict()
149149
sb.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
150150
onPersistScript = sb.ToArray();
151151
}
152-
TransactionState[] transactionStates;
153152
using (ApplicationEngine engine2 = ApplicationEngine.Create(TriggerType.OnPersist, null, snapshot, block, TestBlockchain.TheNeoSystem.Settings, 0))
154153
{
155154
engine2.LoadScript(onPersistScript);
156-
if (engine2.Execute() != VMState.HALT) throw new InvalidOperationException();
157-
Blockchain.ApplicationExecuted application_executed = new(engine2);
158-
transactionStates = engine2.GetState<TransactionState[]>();
155+
if (engine2.Execute() != VMState.HALT) throw engine2.FaultException;
156+
engine2.Snapshot.Commit();
157+
}
158+
snapshot.Commit();
159+
160+
// Run PostPersist to update current block index in native Ledger.
161+
// Relevant current block index is needed for conflict records checks.
162+
byte[] postPersistScript;
163+
using (ScriptBuilder sb = new())
164+
{
165+
sb.EmitSysCall(ApplicationEngine.System_Contract_NativePostPersist);
166+
postPersistScript = sb.ToArray();
167+
}
168+
using (ApplicationEngine engine2 = ApplicationEngine.Create(TriggerType.PostPersist, null, snapshot, block, TestBlockchain.TheNeoSystem.Settings, 0))
169+
{
170+
engine2.LoadScript(postPersistScript);
171+
if (engine2.Execute() != VMState.HALT) throw engine2.FaultException;
159172
engine2.Snapshot.Commit();
160173
}
161174
snapshot.Commit();

tests/Neo.UnitTests/Ledger/UT_MemoryPool.cs

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -736,74 +736,6 @@ public void TestUpdatePoolForBlockPersisted()
736736
_unit.VerifiedCount.Should().Be(0);
737737
}
738738

739-
740-
[TestMethod]
741-
public async Task Malicious_OnChain_Conflict()
742-
{
743-
// Arrange: prepare mempooled txs that have conflicts.
744-
long txFee = 1;
745-
var snapshot = GetSnapshot();
746-
BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, senderAccount);
747-
ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings, gas: long.MaxValue);
748-
engine.LoadScript(Array.Empty<byte>());
749-
750-
var tx1 = CreateTransactionWithFeeAndBalanceVerify(txFee);
751-
var tx2 = CreateTransactionWithFeeAndBalanceVerify(txFee);
752-
753-
tx1.Signers[0].Account = UInt160.Parse("0x0001020304050607080900010203040506070809"); // Different sender
754-
755-
await NativeContract.GAS.Mint(engine, tx1.Sender, 100000, true); // balance enough for all mempooled txs
756-
await NativeContract.GAS.Mint(engine, tx2.Sender, 100000, true); // balance enough for all mempooled txs
757-
758-
tx1.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = tx2.Hash } };
759-
760-
Assert.AreEqual(_unit.TryAdd(tx1, engine.Snapshot), VerifyResult.Succeed);
761-
762-
var block = new Block
763-
{
764-
Header = new Header()
765-
{
766-
Index = 10000,
767-
MerkleRoot = UInt256.Zero,
768-
NextConsensus = UInt160.Zero,
769-
PrevHash = UInt256.Zero,
770-
Witness = new Witness() { InvocationScript = Array.Empty<byte>(), VerificationScript = Array.Empty<byte>() }
771-
},
772-
Transactions = new Transaction[] { tx1 },
773-
};
774-
_unit.UpdatePoolForBlockPersisted(block, engine.Snapshot);
775-
776-
_unit.SortedTxCount.Should().Be(0);
777-
778-
// Simulate persist tx1
779-
780-
Assert.AreEqual(_unit.TryAdd(tx2, engine.Snapshot), VerifyResult.Succeed);
781-
782-
byte[] onPersistScript;
783-
using (ScriptBuilder sb = new())
784-
{
785-
sb.EmitSysCall(ApplicationEngine.System_Contract_NativeOnPersist);
786-
onPersistScript = sb.ToArray();
787-
}
788-
789-
TransactionState[] transactionStates;
790-
using (ApplicationEngine engine2 = ApplicationEngine.Create(TriggerType.OnPersist, null, engine.Snapshot, block, TestBlockchain.TheNeoSystem.Settings, 0))
791-
{
792-
engine2.LoadScript(onPersistScript);
793-
if (engine2.Execute() != VMState.HALT) throw new InvalidOperationException();
794-
Blockchain.ApplicationExecuted application_executed = new(engine2);
795-
transactionStates = engine2.GetState<TransactionState[]>();
796-
}
797-
798-
// Test tx2 arrive
799-
800-
snapshot.Commit();
801-
802-
var senderProbe = CreateTestProbe();
803-
senderProbe.Send(TestBlockchain.TheNeoSystem.Blockchain, tx2);
804-
senderProbe.ExpectMsg<Blockchain.RelayResult>(p => p.Result == VerifyResult.InsufficientFunds); // should be Succedded.
805-
}
806-
807739
public static StorageKey CreateStorageKey(int id, byte prefix, byte[] key = null)
808740
{
809741
byte[] buffer = GC.AllocateUninitializedArray<byte>(sizeof(byte) + (key?.Length ?? 0));

tests/Neo.UnitTests/Ledger/UT_TransactionState.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,7 @@ public void Initialize()
3636
};
3737
originTrimmed = new TransactionState
3838
{
39-
ConflictingSigners = new UInt160[]
40-
{
41-
new UInt160(Crypto.Hash160(new byte[] { 1, 2, 3 })),
42-
new UInt160(Crypto.Hash160(new byte[] { 4, 5, 6 }))
43-
}
39+
BlockIndex = 1,
4440
};
4541
}
4642

@@ -67,10 +63,9 @@ public void TestDeserializeTrimmed()
6763
TransactionState dest = new();
6864
((IInteroperable)dest).FromStackItem(BinarySerializer.Deserialize(ref reader, ExecutionEngineLimits.Default, null));
6965

70-
dest.BlockIndex.Should().Be(0);
66+
dest.BlockIndex.Should().Be(originTrimmed.BlockIndex);
7167
dest.Transaction.Should().Be(null);
7268
dest.Transaction.Should().BeNull();
73-
CollectionAssert.AreEqual(originTrimmed.ConflictingSigners, dest.ConflictingSigners);
7469
}
7570
}
7671
}

0 commit comments

Comments
 (0)