diff --git a/src/neo/Ledger/Blockchain.cs b/src/neo/Ledger/Blockchain.cs index 2c9981f348..c7ce9a412a 100644 --- a/src/neo/Ledger/Blockchain.cs +++ b/src/neo/Ledger/Blockchain.cs @@ -26,6 +26,7 @@ public class Import { public IEnumerable Blocks; public bool Verify = tru public class ImportCompleted { } public class FillMemoryPool { public IEnumerable Transactions; } public class FillCompleted { } + internal class PreverifyCompleted { public Transaction Transaction; public VerifyResult Result; public bool Relay; } public class RelayResult { public IInventory Inventory; public VerifyResult Result; } public static readonly uint MillisecondsPerBlock = ProtocolSettings.Default.MillisecondsPerBlock; @@ -58,6 +59,7 @@ public class RelayResult { public IInventory Inventory; public VerifyResult Resu private const int MaxTxToReverifyPerIdle = 10; private static readonly object lockObj = new object(); private readonly NeoSystem system; + private readonly IActorRef txrouter; private readonly List header_index = new List(); private uint stored_header_count = 0; private readonly Dictionary block_cache = new Dictionary(); @@ -103,6 +105,7 @@ static Blockchain() public Blockchain(NeoSystem system, IStore store) { this.system = system; + this.txrouter = Context.ActorOf(TransactionRouter.Props(system)); this.MemPool = new MemoryPool(system, ProtocolSettings.Default.MemoryPoolMaxTransactions); this.Store = store; this.View = new ReadOnlyView(store); @@ -302,20 +305,15 @@ private void OnFillMemoryPool(IEnumerable transactions) private void OnInventory(IInventory inventory, bool relay = true) { - RelayResult rr = new RelayResult + VerifyResult result = inventory switch { - Inventory = inventory, - Result = inventory switch - { - Block block => OnNewBlock(block), - Transaction transaction => OnNewTransaction(transaction), - _ => OnNewInventory(inventory) - } + Block block => OnNewBlock(block), + Transaction transaction => OnNewTransaction(transaction), + _ => OnNewInventory(inventory) }; - if (relay && rr.Result == VerifyResult.Succeed) + if (relay && result == VerifyResult.Succeed) system.LocalNode.Tell(new LocalNode.RelayDirectly { Inventory = inventory }); - Sender.Tell(rr); - Context.System.EventStream.Publish(rr); + SendRelayResult(inventory, result); } private VerifyResult OnNewBlock(Block block) @@ -360,6 +358,14 @@ private VerifyResult OnNewTransaction(Transaction transaction) return MemPool.TryAdd(transaction, currentSnapshot); } + private void OnPreverifyCompleted(PreverifyCompleted task) + { + if (task.Result == VerifyResult.Succeed) + OnInventory(task.Transaction, task.Relay); + else + SendRelayResult(task.Transaction, task.Result); + } + protected override void OnReceive(object message) { switch (message) @@ -373,15 +379,19 @@ protected override void OnReceive(object message) case Block block: OnInventory(block, false); break; + case Transaction tx: + OnTransaction(tx, true); + break; case Transaction[] transactions: - { - // This message comes from a mempool's revalidation, already relayed - foreach (var tx in transactions) OnInventory(tx, false); - break; - } + // This message comes from a mempool's revalidation, already relayed + foreach (var tx in transactions) OnTransaction(tx, false); + break; case IInventory inventory: OnInventory(inventory); break; + case PreverifyCompleted task: + OnPreverifyCompleted(task); + break; case Idle _: if (MemPool.ReVerifyTopUnverifiedTransactionsIfNeeded(MaxTxToReverifyPerIdle, currentSnapshot)) Self.Tell(Idle.Instance, ActorRefs.NoSender); @@ -389,6 +399,14 @@ protected override void OnReceive(object message) } } + private void OnTransaction(Transaction tx, bool relay) + { + if (ContainsTransaction(tx.Hash)) + SendRelayResult(tx, VerifyResult.AlreadyExists); + else + txrouter.Tell(new TransactionRouter.Task { Transaction = tx, Relay = relay }, Sender); + } + private void Persist(Block block) { using (SnapshotView snapshot = GetSnapshot()) @@ -507,6 +525,17 @@ private void SaveHeaderHashList(SnapshotView snapshot = null) } } + private void SendRelayResult(IInventory inventory, VerifyResult result) + { + RelayResult rr = new RelayResult + { + Inventory = inventory, + Result = result + }; + Sender.Tell(rr); + Context.System.EventStream.Publish(rr); + } + private void UpdateCurrentSnapshot() { Interlocked.Exchange(ref currentSnapshot, GetSnapshot())?.Dispose(); diff --git a/src/neo/Ledger/MemoryPool.cs b/src/neo/Ledger/MemoryPool.cs index 9a8b42b6e6..2cf471a21e 100644 --- a/src/neo/Ledger/MemoryPool.cs +++ b/src/neo/Ledger/MemoryPool.cs @@ -271,7 +271,7 @@ internal VerifyResult TryAdd(Transaction tx, StoreView snapshot) _txRwLock.EnterWriteLock(); try { - VerifyResult result = tx.Verify(snapshot, VerificationContext); + VerifyResult result = tx.VerifyStateDependent(snapshot, VerificationContext); if (result != VerifyResult.Succeed) return result; _unsortedTransactions.Add(tx.Hash, poolItem); @@ -425,7 +425,7 @@ private int ReverifyTransactions(SortedSet verifiedSortedTxPool, // Since unverifiedSortedTxPool is ordered in an ascending manner, we take from the end. foreach (PoolItem item in unverifiedSortedTxPool.Reverse().Take(count)) { - if (item.Tx.VerifyForEachBlock(snapshot, VerificationContext) == VerifyResult.Succeed) + if (item.Tx.VerifyStateDependent(snapshot, VerificationContext) == VerifyResult.Succeed) { reverifiedItems.Add(item); VerificationContext.AddTransaction(item.Tx); diff --git a/src/neo/Ledger/TransactionRouter.cs b/src/neo/Ledger/TransactionRouter.cs new file mode 100644 index 0000000000..8258c46751 --- /dev/null +++ b/src/neo/Ledger/TransactionRouter.cs @@ -0,0 +1,35 @@ +using Akka.Actor; +using Akka.Routing; +using Neo.Network.P2P.Payloads; +using System; + +namespace Neo.Ledger +{ + internal class TransactionRouter : UntypedActor + { + public class Task { public Transaction Transaction; public bool Relay; } + + private readonly IActorRef blockchain; + + public TransactionRouter(NeoSystem system) + { + this.blockchain = system.Blockchain; + } + + protected override void OnReceive(object message) + { + if (!(message is Task task)) return; + blockchain.Tell(new Blockchain.PreverifyCompleted + { + Transaction = task.Transaction, + Result = task.Transaction.VerifyStateIndependent(), + Relay = task.Relay + }, Sender); + } + + internal static Props Props(NeoSystem system) + { + return Akka.Actor.Props.Create(() => new TransactionRouter(system)).WithRouter(new SmallestMailboxPool(Environment.ProcessorCount)); + } + } +} diff --git a/src/neo/Network/P2P/Payloads/Transaction.cs b/src/neo/Network/P2P/Payloads/Transaction.cs index 6c610344bf..02ae5e775b 100644 --- a/src/neo/Network/P2P/Payloads/Transaction.cs +++ b/src/neo/Network/P2P/Payloads/Transaction.cs @@ -281,7 +281,7 @@ bool IInventory.Verify(StoreView snapshot) return Verify(snapshot, null) == VerifyResult.Succeed; } - public virtual VerifyResult VerifyForEachBlock(StoreView snapshot, TransactionVerificationContext context) + public virtual VerifyResult VerifyStateDependent(StoreView snapshot, TransactionVerificationContext context) { if (ValidUntilBlock <= snapshot.Height || ValidUntilBlock > snapshot.Height + MaxValidUntilBlockIncrement) return VerifyResult.Expired; @@ -294,24 +294,27 @@ public virtual VerifyResult VerifyForEachBlock(StoreView snapshot, TransactionVe foreach (TransactionAttribute attribute in Attributes) if (!attribute.Verify(snapshot, this)) return VerifyResult.Invalid; - if (hashes.Length != Witnesses.Length) return VerifyResult.Invalid; - for (int i = 0; i < hashes.Length; i++) - { - if (Witnesses[i].VerificationScript.Length > 0) continue; - if (snapshot.Contracts.TryGet(hashes[i]) is null) return VerifyResult.Invalid; - } + long net_fee = NetworkFee - Size * NativeContract.Policy.GetFeePerByte(snapshot); + if (!this.VerifyWitnesses(snapshot, net_fee, WitnessFlag.StateDependent)) + return VerifyResult.Invalid; + return VerifyResult.Succeed; + } + + public virtual VerifyResult VerifyStateIndependent() + { + if (Size > MaxTransactionSize) + return VerifyResult.Invalid; + if (!this.VerifyWitnesses(null, NetworkFee, WitnessFlag.StateIndependent)) + return VerifyResult.Invalid; return VerifyResult.Succeed; } public virtual VerifyResult Verify(StoreView snapshot, TransactionVerificationContext context) { - VerifyResult result = VerifyForEachBlock(snapshot, context); + VerifyResult result = VerifyStateIndependent(); if (result != VerifyResult.Succeed) return result; - if (Size > MaxTransactionSize) return VerifyResult.Invalid; - long net_fee = NetworkFee - Size * NativeContract.Policy.GetFeePerByte(snapshot); - if (net_fee < 0) return VerifyResult.InsufficientFunds; - if (!this.VerifyWitnesses(snapshot, net_fee)) return VerifyResult.Invalid; - return VerifyResult.Succeed; + result = VerifyStateDependent(snapshot, context); + return result; } public StackItem ToStackItem(ReferenceCounter referenceCounter) diff --git a/src/neo/Network/P2P/Payloads/Witness.cs b/src/neo/Network/P2P/Payloads/Witness.cs index 34121a6a6a..d54158050d 100644 --- a/src/neo/Network/P2P/Payloads/Witness.cs +++ b/src/neo/Network/P2P/Payloads/Witness.cs @@ -11,6 +11,8 @@ public class Witness : ISerializable public byte[] InvocationScript; public byte[] VerificationScript; + internal long GasConsumed { get; set; } + private UInt160 _scriptHash; public virtual UInt160 ScriptHash { @@ -24,6 +26,8 @@ public virtual UInt160 ScriptHash } } + public bool StateDependent => VerificationScript.Length == 0; + public int Size => InvocationScript.GetVarSize() + VerificationScript.GetVarSize(); void ISerializable.Deserialize(BinaryReader reader) diff --git a/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs b/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs index 530b777601..0cddbd49d7 100644 --- a/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs +++ b/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs @@ -286,13 +286,19 @@ private void OnGetHeadersMessageReceived(GetBlockByIndexPayload payload) private void OnInventoryReceived(IInventory inventory) { - system.TaskManager.Tell(inventory); - if (inventory is Transaction transaction) - system.Consensus?.Tell(transaction); - system.Blockchain.Tell(inventory, ActorRefs.NoSender); pendingKnownHashes.Remove(inventory.Hash); knownHashes.Add(inventory.Hash); - if (inventory is Block b) UpdateLastBlockIndex(b.Index, false); + system.TaskManager.Tell(inventory); + system.Blockchain.Tell(inventory, ActorRefs.NoSender); + switch (inventory) + { + case Transaction transaction: + system.Consensus?.Tell(transaction); + break; + case Block block: + UpdateLastBlockIndex(block.Index, false); + break; + } } private void OnInvMessageReceived(InvPayload payload) diff --git a/src/neo/SmartContract/Helper.cs b/src/neo/SmartContract/Helper.cs index 2c0bacacb6..202cd862e3 100644 --- a/src/neo/SmartContract/Helper.cs +++ b/src/neo/SmartContract/Helper.cs @@ -129,7 +129,7 @@ public static UInt160 ToScriptHash(this ReadOnlySpan script) return new UInt160(Crypto.Hash160(script)); } - internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snapshot, long gas) + internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snapshot, long gas, WitnessFlag filter = WitnessFlag.All) { if (gas < 0) return false; if (gas > MaxVerificationGas) gas = MaxVerificationGas; @@ -146,6 +146,14 @@ internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snap if (hashes.Length != verifiable.Witnesses.Length) return false; for (int i = 0; i < hashes.Length; i++) { + WitnessFlag flag = verifiable.Witnesses[i].StateDependent ? WitnessFlag.StateDependent : WitnessFlag.StateIndependent; + if (!filter.HasFlag(flag)) + { + gas -= verifiable.Witnesses[i].GasConsumed; + if (gas < 0) return false; + continue; + } + int offset; ContractMethodDescriptor init = null; byte[] verification = verifiable.Witnesses[i].VerificationScript; @@ -164,14 +172,16 @@ internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snap if (hashes[i] != verifiable.Witnesses[i].ScriptHash) return false; offset = 0; } - using (ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, verifiable, snapshot.Clone(), gas)) + using (ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, verifiable, snapshot?.Clone(), gas)) { - ExecutionContext context = engine.LoadScript(verification, CallFlags.None, offset); + CallFlags callFlags = verifiable.Witnesses[i].StateDependent ? CallFlags.AllowStates : CallFlags.None; + ExecutionContext context = engine.LoadScript(verification, callFlags, offset); if (init != null) engine.LoadContext(context.Clone(init.Offset), false); engine.LoadScript(verifiable.Witnesses[i].InvocationScript, CallFlags.None); if (engine.Execute() == VMState.FAULT) return false; if (engine.ResultStack.Count != 1 || !engine.ResultStack.Pop().GetBoolean()) return false; gas -= engine.GasConsumed; + verifiable.Witnesses[i].GasConsumed = engine.GasConsumed; } } return true; diff --git a/src/neo/SmartContract/WitnessFlag.cs b/src/neo/SmartContract/WitnessFlag.cs new file mode 100644 index 0000000000..528562a70f --- /dev/null +++ b/src/neo/SmartContract/WitnessFlag.cs @@ -0,0 +1,15 @@ +using System; + +namespace Neo.SmartContract +{ + [Flags] + internal enum WitnessFlag : byte + { + None = 0, + + StateIndependent = 0b00000001, + StateDependent = 0b00000010, + + All = StateIndependent | StateDependent + } +} diff --git a/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs b/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs index eabe2cc9a6..04f70db038 100644 --- a/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs +++ b/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs @@ -9,6 +9,7 @@ using Neo.Plugins; using Neo.SmartContract; using Neo.SmartContract.Native; +using Neo.SmartContract.Native.Tokens; using System; using System.Collections; using System.Collections.Generic; @@ -31,6 +32,7 @@ public class UT_MemoryPool private const byte Prefix_MaxTransactionsPerBlock = 23; private const byte Prefix_FeePerByte = 10; + private readonly UInt160 senderAccount = UInt160.Zero; private MemoryPool _unit; private MemoryPool _unit2; private TestIMemoryPoolTxObserverPlugin plugin; @@ -82,12 +84,13 @@ private Transaction CreateTransactionWithFee(long fee) var randomBytes = new byte[16]; random.NextBytes(randomBytes); Mock mock = new Mock(); - mock.Setup(p => p.VerifyForEachBlock(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); mock.Setup(p => p.Verify(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateIndependent()).Returns(VerifyResult.Succeed); mock.Object.Script = randomBytes; mock.Object.NetworkFee = fee; mock.Object.Attributes = Array.Empty(); - mock.Object.Signers = new Signer[] { new Signer() { Account = UInt160.Zero, Scopes = WitnessScope.None } }; + mock.Object.Signers = new Signer[] { new Signer() { Account = senderAccount, Scopes = WitnessScope.None } }; mock.Object.Witnesses = new[] { new Witness @@ -105,12 +108,14 @@ private Transaction CreateTransactionWithFeeAndBalanceVerify(long fee) var randomBytes = new byte[16]; random.NextBytes(randomBytes); Mock mock = new Mock(); - mock.Setup(p => p.VerifyForEachBlock(It.IsAny(), It.IsAny())).Returns((StoreView snapshot, TransactionVerificationContext context) => context.CheckTransaction(mock.Object, snapshot) ? VerifyResult.Succeed : VerifyResult.InsufficientFunds); + UInt160 sender = senderAccount; mock.Setup(p => p.Verify(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny())).Returns((StoreView snapshot, TransactionVerificationContext context) => context.CheckTransaction(mock.Object, snapshot) ? VerifyResult.Succeed : VerifyResult.InsufficientFunds); + mock.Setup(p => p.VerifyStateIndependent()).Returns(VerifyResult.Succeed); mock.Object.Script = randomBytes; mock.Object.NetworkFee = fee; mock.Object.Attributes = Array.Empty(); - mock.Object.Signers = new Signer[] { new Signer() { Account = UInt160.Zero, Scopes = WitnessScope.None } }; + mock.Object.Signers = new Signer[] { new Signer() { Account = senderAccount, Scopes = WitnessScope.None } }; mock.Object.Witnesses = new[] { new Witness @@ -147,9 +152,8 @@ private void AddTransaction(Transaction txToAdd) _unit.TryAdd(txToAdd, snapshot); } - private void AddTransactionsWithBalanceVerify(int count, long fee) + private void AddTransactionsWithBalanceVerify(int count, long fee, SnapshotView snapshot) { - var snapshot = Blockchain.Singleton.GetSnapshot(); for (int i = 0; i < count; i++) { var txToAdd = CreateTransactionWithFeeAndBalanceVerify(fee); @@ -218,8 +222,14 @@ public void BlockPersistMovesTxToUnverifiedAndReverification() [TestMethod] public void BlockPersistAndReverificationWillAbandonTxAsBalanceTransfered() { + SnapshotView snapshot = Blockchain.Singleton.GetSnapshot(); + BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, senderAccount); + ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, long.MaxValue); + NativeContract.GAS.Burn(engine, UInt160.Zero, balance); + NativeContract.GAS.Mint(engine, UInt160.Zero, 70); + long txFee = 1; - AddTransactionsWithBalanceVerify(70, txFee); + AddTransactionsWithBalanceVerify(70, txFee, snapshot); _unit.SortedTxCount.Should().Be(70); @@ -230,11 +240,9 @@ public void BlockPersistAndReverificationWillAbandonTxAsBalanceTransfered() // Simulate the transfer process in tx by burning the balance UInt160 sender = block.Transactions[0].Sender; - SnapshotView snapshot = Blockchain.Singleton.GetSnapshot(); - BigInteger balance = NativeContract.GAS.BalanceOf(snapshot, sender); ApplicationEngine applicationEngine = ApplicationEngine.Create(TriggerType.All, block, snapshot, (long)balance); - NativeContract.GAS.Burn(applicationEngine, sender, balance); + NativeContract.GAS.Burn(applicationEngine, sender, NativeContract.GAS.BalanceOf(snapshot, sender)); NativeContract.GAS.Mint(applicationEngine, sender, txFee * 30); // Set the balance to meet 30 txs only // Persist block and reverify all the txs in mempool, but half of the txs will be discarded diff --git a/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs b/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs index a3636a4c91..a34a8cfb91 100644 --- a/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs +++ b/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs @@ -28,8 +28,9 @@ private Transaction CreateTransactionWithFee(long networkFee, long systemFee) var randomBytes = new byte[16]; random.NextBytes(randomBytes); Mock mock = new Mock(); - mock.Setup(p => p.VerifyForEachBlock(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); mock.Setup(p => p.Verify(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateIndependent()).Returns(VerifyResult.Succeed); mock.Object.Script = randomBytes; mock.Object.NetworkFee = networkFee; mock.Object.SystemFee = systemFee; diff --git a/tests/neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs b/tests/neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs index 9f80f21e22..c8e4c4e028 100644 --- a/tests/neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs +++ b/tests/neo.UnitTests/Network/P2P/Payloads/UT_Transaction.cs @@ -773,7 +773,7 @@ public void Transaction_Reverify_Hashes_Length_Unequal_To_Witnesses_Length() }; UInt160[] hashes = txSimple.GetScriptHashesForVerifying(snapshot); Assert.AreEqual(1, hashes.Length); - Assert.AreNotEqual(VerifyResult.Succeed, txSimple.VerifyForEachBlock(snapshot, new TransactionVerificationContext())); + Assert.AreNotEqual(VerifyResult.Succeed, txSimple.VerifyStateDependent(snapshot, new TransactionVerificationContext())); } [TestMethod] @@ -1073,5 +1073,219 @@ public void ToJson() jObj["script"].AsString().Should().Be("QiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA="); jObj["sysfee"].AsString().Should().Be("4200000000"); } + + [TestMethod] + public void Test_VerifyStateIndependent() + { + var tx = new Transaction() + { + Attributes = Array.Empty(), + NetworkFee = 0, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize], + Signers = new Signer[] { new Signer() { Account = UInt160.Zero } }, + SystemFee = 0, + ValidUntilBlock = 0, + Version = 0, + Witnesses = new Witness[0], + }; + tx.VerifyStateIndependent().Should().Be(VerifyResult.Invalid); + tx.Script = new byte[0]; + tx.VerifyStateIndependent().Should().Be(VerifyResult.Invalid); + + var walletA = TestUtils.GenerateTestWallet(); + var walletB = TestUtils.GenerateTestWallet(); + var snapshot = Blockchain.Singleton.GetSnapshot(); + + using (var unlockA = walletA.Unlock("123")) + using (var unlockB = walletB.Unlock("123")) + { + var a = walletA.CreateAccount(); + var b = walletB.CreateAccount(); + + var multiSignContract = Contract.CreateMultiSigContract(2, + new ECPoint[] + { + a.GetKey().PublicKey, + b.GetKey().PublicKey + }); + + walletA.CreateAccount(multiSignContract, a.GetKey()); + var acc = walletB.CreateAccount(multiSignContract, b.GetKey()); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + var entry = snapshot.Storages.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshot.Commit(); + + // Make transaction + + tx = walletA.MakeTransaction(new TransferOutput[] + { + new TransferOutput() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(1,8) + } + }, acc.ScriptHash); + + // Sign + + var data = new ContractParametersContext(tx); + Assert.IsTrue(walletA.Sign(data)); + Assert.IsTrue(walletB.Sign(data)); + Assert.IsTrue(data.Completed); + + tx.Witnesses = data.GetWitnesses(); + tx.VerifyStateIndependent().Should().Be(VerifyResult.Succeed); + } + } + + [TestMethod] + public void Test_VerifyStateDependent() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + var tx = new Transaction() + { + Attributes = Array.Empty(), + NetworkFee = 0, + Nonce = (uint)Environment.TickCount, + Script = new byte[0], + Signers = new Signer[] { new Signer() { Account = UInt160.Zero } }, + SystemFee = 0, + ValidUntilBlock = snapshot.Height + 1, + Version = 0, + Witnesses = new Witness[0], + }; + tx.VerifyStateDependent(snapshot, new TransactionVerificationContext()).Should().Be(VerifyResult.Invalid); + tx.SystemFee = 10; + tx.VerifyStateDependent(snapshot, new TransactionVerificationContext()).Should().Be(VerifyResult.InsufficientFunds); + + var walletA = TestUtils.GenerateTestWallet(); + var walletB = TestUtils.GenerateTestWallet(); + + using (var unlockA = walletA.Unlock("123")) + using (var unlockB = walletB.Unlock("123")) + { + var a = walletA.CreateAccount(); + var b = walletB.CreateAccount(); + + var multiSignContract = Contract.CreateMultiSigContract(2, + new ECPoint[] + { + a.GetKey().PublicKey, + b.GetKey().PublicKey + }); + + walletA.CreateAccount(multiSignContract, a.GetKey()); + var acc = walletB.CreateAccount(multiSignContract, b.GetKey()); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + var entry = snapshot.Storages.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshot.Commit(); + + // Make transaction + + tx = walletA.MakeTransaction(new TransferOutput[] + { + new TransferOutput() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(1,8) + } + }, acc.ScriptHash); + + // Sign + + var data = new ContractParametersContext(tx); + Assert.IsTrue(walletA.Sign(data)); + Assert.IsTrue(walletB.Sign(data)); + Assert.IsTrue(data.Completed); + + tx.Witnesses = data.GetWitnesses(); + tx.VerifyStateDependent(snapshot, new TransactionVerificationContext()).Should().Be(VerifyResult.Succeed); + } + } + + [TestMethod] + public void Test_Verify() + { + var snapshot = Blockchain.Singleton.GetSnapshot(); + var tx = new Transaction() + { + Attributes = Array.Empty(), + NetworkFee = 0, + Nonce = (uint)Environment.TickCount, + Script = new byte[Transaction.MaxTransactionSize], + Signers = new Signer[] { new Signer() { Account = UInt160.Zero } }, + SystemFee = 0, + ValidUntilBlock = 0, + Version = 0, + Witnesses = new Witness[0], + }; + tx.Verify(snapshot, new TransactionVerificationContext()).Should().Be(VerifyResult.Invalid); + + var walletA = TestUtils.GenerateTestWallet(); + var walletB = TestUtils.GenerateTestWallet(); + + using (var unlockA = walletA.Unlock("123")) + using (var unlockB = walletB.Unlock("123")) + { + var a = walletA.CreateAccount(); + var b = walletB.CreateAccount(); + + var multiSignContract = Contract.CreateMultiSigContract(2, + new ECPoint[] + { + a.GetKey().PublicKey, + b.GetKey().PublicKey + }); + + walletA.CreateAccount(multiSignContract, a.GetKey()); + var acc = walletB.CreateAccount(multiSignContract, b.GetKey()); + + // Fake balance + + var key = NativeContract.GAS.CreateStorageKey(20, acc.ScriptHash); + var entry = snapshot.Storages.GetAndChange(key, () => new StorageItem(new AccountState())); + + entry.GetInteroperable().Balance = 10000 * NativeContract.GAS.Factor; + + snapshot.Commit(); + + // Make transaction + + tx = walletA.MakeTransaction(new TransferOutput[] + { + new TransferOutput() + { + AssetId = NativeContract.GAS.Hash, + ScriptHash = acc.ScriptHash, + Value = new BigDecimal(1,8) + } + }, acc.ScriptHash); + + // Sign + + var data = new ContractParametersContext(tx); + Assert.IsTrue(walletA.Sign(data)); + Assert.IsTrue(walletB.Sign(data)); + Assert.IsTrue(data.Completed); + + tx.Witnesses = data.GetWitnesses(); + tx.Verify(snapshot, new TransactionVerificationContext()).Should().Be(VerifyResult.Succeed); + } + } } }