Skip to content

Commit fd1748d

Browse files
committed
Check signers of on-chained conflict during new tx verification
Fix the problem described in neo-project#2818 (review). During new transaction verification if there's an on-chain conflicting transaction, we should check the signers of this conflicting transaction. If the signers contain payer of the incoming transaction, then the conflict is treated as valid and verification for new incoming transaction should fail. Otherwise, the conflict is treated as the malicious attack attempt and will not be taken into account; verification for the new incoming transaction should continue in this case.
1 parent b72a4ad commit fd1748d

File tree

8 files changed

+49
-20
lines changed

8 files changed

+49
-20
lines changed

src/Neo/Ledger/Blockchain.cs

+3-3
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))
209+
if (NativeContract.Ledger.ContainsConflictHash(snapshot, tx.Hash, tx.Sender))
210210
continue;
211211
// First remove the tx if it is unverified in the pool.
212212
system.MemPool.TryRemoveUnVerified(tx.Hash, out _);
@@ -339,7 +339,7 @@ private VerifyResult OnNewExtensiblePayload(ExtensiblePayload payload)
339339
private VerifyResult OnNewTransaction(Transaction transaction)
340340
{
341341
if (system.ContainsTransaction(transaction.Hash)) return VerifyResult.AlreadyExists;
342-
if (system.ContainsConflictHash(transaction.Hash)) return VerifyResult.HasConflicts;
342+
if (system.ContainsConflictHash(transaction.Hash, transaction.Sender)) return VerifyResult.HasConflicts;
343343
return system.MemPool.TryAdd(transaction, system.StoreView);
344344
}
345345

@@ -394,7 +394,7 @@ private void OnTransaction(Transaction tx)
394394
{
395395
if (system.ContainsTransaction(tx.Hash))
396396
SendRelayResult(tx, VerifyResult.AlreadyExists);
397-
else if (system.ContainsConflictHash(tx.Hash))
397+
else if (system.ContainsConflictHash(tx.Hash, tx.Sender))
398398
SendRelayResult(tx, VerifyResult.HasConflicts);
399399
else system.TxRouter.Forward(new TransactionRouter.Preverify(tx, true));
400400
}

src/Neo/Ledger/MemoryPool.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -469,15 +469,16 @@ internal void UpdatePoolForBlockPersisted(Block block, DataCache snapshot)
469469
_txRwLock.EnterWriteLock();
470470
try
471471
{
472-
HashSet<UInt256> conflicts = new HashSet<UInt256>();
472+
Dictionary<UInt256, UInt160[]> conflicts = new Dictionary<UInt256, UInt160[]>();
473473
// First remove the transactions verified in the block.
474474
// No need to modify VerificationContext as it will be reset afterwards.
475475
foreach (Transaction tx in block.Transactions)
476476
{
477477
if (!TryRemoveVerified(tx.Hash, out _)) TryRemoveUnVerified(tx.Hash, out _);
478+
UInt160[] conflictingSigners = tx.Signers.Select(s => s.Account).ToArray();
478479
foreach (var h in tx.GetAttributes<Conflicts>().Select(a => a.Hash))
479480
{
480-
conflicts.Add(h);
481+
conflicts.Add(h, conflictingSigners);
481482
}
482483
}
483484

@@ -487,7 +488,7 @@ internal void UpdatePoolForBlockPersisted(Block block, DataCache snapshot)
487488
var stale = new List<UInt256>();
488489
foreach (var item in _sortedTransactions)
489490
{
490-
if (conflicts.Contains(item.Tx.Hash) || item.Tx.GetAttributes<Conflicts>().Select(a => a.Hash).Intersect(persisted).Any())
491+
if ((conflicts.TryGetValue(item.Tx.Hash, out var signers) && signers.Contains(item.Tx.Sender)) || item.Tx.GetAttributes<Conflicts>().Select(a => a.Hash).Intersect(persisted).Any())
491492
{
492493
stale.Add(item.Tx.Hash);
493494
conflictingItems.Add(item.Tx);

src/Neo/NeoSystem.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,11 @@ public bool ContainsTransaction(UInt256 hash)
283283
/// Determines whether the specified transaction conflicts with some on-chain transaction.
284284
/// </summary>
285285
/// <param name="hash">The hash of the transaction</param>
286+
/// <param name="sender">The sender of the transaction</param>
286287
/// <returns><see langword="true"/> if the transaction conflicts with on-chain transaction; otherwise, <see langword="false"/>.</returns>
287-
public bool ContainsConflictHash(UInt256 hash)
288+
public bool ContainsConflictHash(UInt256 hash, UInt160 sender)
288289
{
289-
return NativeContract.Ledger.ContainsConflictHash(StoreView, hash);
290+
return NativeContract.Ledger.ContainsConflictHash(StoreView, hash, sender);
290291
}
291292
}
292293
}

src/Neo/Network/P2P/RemoteNode.ProtocolHandler.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ private void OnInventoryReceived(IInventory inventory)
318318
switch (inventory)
319319
{
320320
case Transaction transaction:
321-
if (!(system.ContainsTransaction(transaction.Hash) || system.ContainsConflictHash(transaction.Hash)))
321+
if (!(system.ContainsTransaction(transaction.Hash) || system.ContainsConflictHash(transaction.Hash, transaction.Sender)))
322322
system.TxRouter.Tell(new TransactionRouter.Preverify(transaction, true));
323323
break;
324324
case Block block:
@@ -347,7 +347,7 @@ private void OnInvMessageReceived(InvPayload payload)
347347
case InventoryType.TX:
348348
{
349349
DataCache snapshot = system.StoreView;
350-
hashes = hashes.Where(p => !(NativeContract.Ledger.ContainsTransaction(snapshot, p) || NativeContract.Ledger.ContainsConflictHash(snapshot, p))).ToArray();
350+
hashes = hashes.Where(p => !(NativeContract.Ledger.ContainsTransaction(snapshot, p) || NativeContract.Ledger.ContainsConflictHash(snapshot, p, null))).ToArray();
351351
}
352352
break;
353353
}

src/Neo/SmartContract/Native/LedgerContract.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ internal override ContractTask OnPersist(ApplicationEngine engine)
4747
foreach (TransactionState tx in transactions)
4848
{
4949
engine.Snapshot.Add(CreateStorageKey(Prefix_Transaction).Add(tx.Transaction.Hash), new StorageItem(tx));
50+
UInt160[] conflictingSigners = tx.Transaction.Signers.Select(s => s.Account).ToArray();
5051
foreach (var attr in tx.Transaction.GetAttributes<Conflicts>())
5152
{
52-
engine.Snapshot.GetOrAdd(CreateStorageKey(Prefix_Transaction).Add(attr.Hash), () => new StorageItem(new TransactionState()));
53+
engine.Snapshot.GetOrAdd(CreateStorageKey(Prefix_Transaction).Add(attr.Hash), () => new StorageItem(new TransactionState { ConflictingSigners = conflictingSigners }));
5354
}
5455
}
5556
engine.SetState(transactions);
@@ -140,11 +141,12 @@ public bool ContainsTransaction(DataCache snapshot, UInt256 hash)
140141
/// </summary>
141142
/// <param name="snapshot">The snapshot used to read data.</param>
142143
/// <param name="hash">The hash of the conflicting transaction.</param>
144+
/// <param name="sender">The sender of the conflicting transaction.</param>
143145
/// <returns><see langword="true"/> if the blockchain contains the hash of the conflicting transaction; otherwise, <see langword="false"/>.</returns>
144-
public bool ContainsConflictHash(DataCache snapshot, UInt256 hash)
146+
public bool ContainsConflictHash(DataCache snapshot, UInt256 hash, UInt160 sender)
145147
{
146148
var state = snapshot.TryGet(CreateStorageKey(Prefix_Transaction).Add(hash))?.GetInteroperable<TransactionState>();
147-
if (state is not null && state.Transaction is null) return true;
149+
if (state is not null && state.Transaction is null && (sender is null || state.ConflictingSigners.Contains(sender))) return true;
148150
return false;
149151
}
150152

src/Neo/SmartContract/Native/TransactionState.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Neo.VM;
1414
using Neo.VM.Types;
1515
using System;
16+
using System.Linq;
1617

1718
namespace Neo.SmartContract.Native
1819
{
@@ -31,6 +32,8 @@ public class TransactionState : IInteroperable
3132
/// </summary>
3233
public Transaction Transaction;
3334

35+
public UInt160[] ConflictingSigners;
36+
3437
/// <summary>
3538
/// The execution state
3639
/// </summary>
@@ -44,6 +47,7 @@ IInteroperable IInteroperable.Clone()
4447
{
4548
BlockIndex = BlockIndex,
4649
Transaction = Transaction,
50+
ConflictingSigners = ConflictingSigners,
4751
State = State,
4852
_rawTransaction = _rawTransaction
4953
};
@@ -54,6 +58,7 @@ void IInteroperable.FromReplica(IInteroperable replica)
5458
TransactionState from = (TransactionState)replica;
5559
BlockIndex = from.BlockIndex;
5660
Transaction = from.Transaction;
61+
ConflictingSigners = from.ConflictingSigners;
5762
State = from.State;
5863
if (_rawTransaction.IsEmpty)
5964
_rawTransaction = from._rawTransaction;
@@ -62,7 +67,11 @@ void IInteroperable.FromReplica(IInteroperable replica)
6267
void IInteroperable.FromStackItem(StackItem stackItem)
6368
{
6469
Struct @struct = (Struct)stackItem;
65-
if (@struct.Count == 0) return;
70+
if (@struct.Count == 1)
71+
{
72+
ConflictingSigners = ((VM.Types.Array)@struct[0]).Select(u => new UInt160(u.GetSpan())).ToArray();
73+
return;
74+
}
6675
BlockIndex = (uint)@struct[0].GetInteger();
6776
_rawTransaction = ((ByteString)@struct[1]).Memory;
6877
Transaction = _rawTransaction.AsSerializable<Transaction>();
@@ -71,7 +80,7 @@ void IInteroperable.FromStackItem(StackItem stackItem)
7180

7281
StackItem IInteroperable.ToStackItem(ReferenceCounter referenceCounter)
7382
{
74-
if (Transaction is null) return new Struct(referenceCounter);
83+
if (Transaction is null) return new Struct(referenceCounter) { new VM.Types.Array(referenceCounter, ConflictingSigners.Select(u => new ByteString(u.ToArray())).ToArray()) };
7584
if (_rawTransaction.IsEmpty)
7685
_rawTransaction = Transaction.ToArray();
7786
return new Struct(referenceCounter) { BlockIndex, _rawTransaction, (byte)State };

tests/Neo.UnitTests/Ledger/UT_MemoryPool.cs

+11-4
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public async Task UpdatePoolForBlockPersisted_RemoveBlockConflicts()
252252
ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings, gas: long.MaxValue);
253253
engine.LoadScript(Array.Empty<byte>());
254254
await NativeContract.GAS.Burn(engine, UInt160.Zero, balance);
255-
_ = NativeContract.GAS.Mint(engine, UInt160.Zero, 6, true); // balance enough for 6 mempooled txs
255+
_ = NativeContract.GAS.Mint(engine, UInt160.Zero, 7, true); // balance enough for 7 mempooled txs
256256

257257
var mp1 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp1 doesn't conflict with anyone
258258
_unit.TryAdd(mp1, engine.Snapshot).Should().Be(VerifyResult.Succeed);
@@ -283,21 +283,28 @@ public async Task UpdatePoolForBlockPersisted_RemoveBlockConflicts()
283283
_unit.SortedTxCount.Should().Be(6);
284284
_unit.UnverifiedSortedTxCount.Should().Be(0);
285285

286+
var mp7 = CreateTransactionWithFeeAndBalanceVerify(txFee); // mp7 doesn't conflict with anyone
287+
var tx6 = CreateTransactionWithFeeAndBalanceVerify(txFee); // in-block tx6 conflicts with mp7, but doesn't include sender of mp7 into signers list => even if tx6 is included into block, mp7 shouldn't be removed from the pool
288+
tx6.Signers = new Signer[] { new Signer() { Account = new UInt160(Crypto.Hash160(new byte[] { 1, 2, 3 })) }, new Signer() { Account = new UInt160(Crypto.Hash160(new byte[] { 4, 5, 6 })) } };
289+
tx6.Attributes = new TransactionAttribute[] { new Conflicts() { Hash = mp7.Hash } };
290+
_unit.TryAdd(mp7, engine.Snapshot);
291+
286292
// Act: persist block and reverify all mempooled txs.
287293
var block = new Block
288294
{
289295
Header = new Header(),
290-
Transactions = new Transaction[] { tx1, tx2, tx3, tx4, tx5 },
296+
Transactions = new Transaction[] { tx1, tx2, tx3, tx4, tx5, tx6 },
291297
};
292298
_unit.UpdatePoolForBlockPersisted(block, engine.Snapshot);
293299

294300
// Assert: conflicting txs should be removed from the pool; the only mp6 that doesn't conflict with anyone should be left.
295-
_unit.SortedTxCount.Should().Be(1);
301+
_unit.SortedTxCount.Should().Be(2);
296302
_unit.GetSortedVerifiedTransactions().Select(tx => tx.Hash).Should().Contain(mp6.Hash);
303+
_unit.GetSortedVerifiedTransactions().Select(tx => tx.Hash).Should().Contain(mp7.Hash);
297304
_unit.UnverifiedSortedTxCount.Should().Be(0);
298305

299306
// Cleanup: revert the balance.
300-
await NativeContract.GAS.Burn(engine, UInt160.Zero, txFee * 6);
307+
await NativeContract.GAS.Burn(engine, UInt160.Zero, txFee * 7);
301308
_ = NativeContract.GAS.Mint(engine, UInt160.Zero, balance, true);
302309
}
303310

tests/Neo.UnitTests/Ledger/UT_TransactionState.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using FluentAssertions;
22
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using Neo.Cryptography;
34
using Neo.IO;
45
using Neo.Network.P2P.Payloads;
56
using Neo.SmartContract;
@@ -33,7 +34,14 @@ public void Initialize()
3334
} }
3435
}
3536
};
36-
originTrimmed = new TransactionState();
37+
originTrimmed = new TransactionState
38+
{
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+
}
44+
};
3745
}
3846

3947
[TestMethod]
@@ -62,6 +70,7 @@ public void TestDeserializeTrimmed()
6270
dest.BlockIndex.Should().Be(0);
6371
dest.Transaction.Should().Be(null);
6472
dest.Transaction.Should().BeNull();
73+
CollectionAssert.AreEqual(originTrimmed.ConflictingSigners, dest.ConflictingSigners);
6574
}
6675
}
6776
}

0 commit comments

Comments
 (0)