diff --git a/src/neo/SmartContract/ApplicationEngine.Runtime.cs b/src/neo/SmartContract/ApplicationEngine.Runtime.cs index 9713d03893..ffa551b00f 100644 --- a/src/neo/SmartContract/ApplicationEngine.Runtime.cs +++ b/src/neo/SmartContract/ApplicationEngine.Runtime.cs @@ -21,7 +21,7 @@ partial class ApplicationEngine public static readonly InteropDescriptor System_Runtime_GetExecutingScriptHash = Register("System.Runtime.GetExecutingScriptHash", nameof(CurrentScriptHash), 0_00000400, CallFlags.None, true); public static readonly InteropDescriptor System_Runtime_GetCallingScriptHash = Register("System.Runtime.GetCallingScriptHash", nameof(CallingScriptHash), 0_00000400, CallFlags.None, true); public static readonly InteropDescriptor System_Runtime_GetEntryScriptHash = Register("System.Runtime.GetEntryScriptHash", nameof(EntryScriptHash), 0_00000400, CallFlags.None, true); - public static readonly InteropDescriptor System_Runtime_CheckWitness = Register("System.Runtime.CheckWitness", nameof(CheckWitness), 0_00030000, CallFlags.AllowStates, true); + public static readonly InteropDescriptor System_Runtime_CheckWitness = Register("System.Runtime.CheckWitness", nameof(CheckWitness), 0_00030000, CallFlags.None, true); public static readonly InteropDescriptor System_Runtime_GetInvocationCounter = Register("System.Runtime.GetInvocationCounter", nameof(GetInvocationCounter), 0_00000400, CallFlags.None, true); public static readonly InteropDescriptor System_Runtime_Log = Register("System.Runtime.Log", nameof(RuntimeLog), 0_01000000, CallFlags.AllowNotify, false); public static readonly InteropDescriptor System_Runtime_Notify = Register("System.Runtime.Notify", nameof(RuntimeNotify), 0_01000000, CallFlags.AllowNotify, false); @@ -109,7 +109,7 @@ internal bool CheckWitnessInternal(UInt160 hash) if (signer.Scopes == WitnessScope.Global) return true; if (signer.Scopes.HasFlag(WitnessScope.CalledByEntry)) { - if (CallingScriptHash == EntryScriptHash) + if (CallingScriptHash == null || CallingScriptHash == EntryScriptHash) return true; } if (signer.Scopes.HasFlag(WitnessScope.CustomContracts)) @@ -119,6 +119,11 @@ internal bool CheckWitnessInternal(UInt160 hash) } if (signer.Scopes.HasFlag(WitnessScope.CustomGroups)) { + // Check allow state callflag + + if (!CurrentContext.GetState().CallFlags.HasFlag(CallFlags.AllowStates)) + throw new InvalidOperationException($"Cannot call this SYSCALL without the flag AllowStates."); + var contract = Snapshot.Contracts[CallingScriptHash]; // check if current group is the required one if (contract.Manifest.Groups.Select(p => p.PubKey).Intersect(signer.AllowedGroups).Any()) @@ -127,6 +132,11 @@ internal bool CheckWitnessInternal(UInt160 hash) return false; } + // Check allow state callflag + + if (!CurrentContext.GetState().CallFlags.HasFlag(CallFlags.AllowStates)) + throw new InvalidOperationException($"Cannot call this SYSCALL without the flag AllowStates."); + // only for non-Transaction types (Block, etc) var hashes_for_verifying = ScriptContainer.GetScriptHashesForVerifying(Snapshot); diff --git a/src/neo/SmartContract/ContractParametersContext.cs b/src/neo/SmartContract/ContractParametersContext.cs index c494fcf994..0b2a3a5447 100644 --- a/src/neo/SmartContract/ContractParametersContext.cs +++ b/src/neo/SmartContract/ContractParametersContext.cs @@ -9,7 +9,6 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Text; namespace Neo.SmartContract { diff --git a/src/neo/SmartContract/DeployedContract.cs b/src/neo/SmartContract/DeployedContract.cs new file mode 100644 index 0000000000..e9b1c42d62 --- /dev/null +++ b/src/neo/SmartContract/DeployedContract.cs @@ -0,0 +1,24 @@ +using Neo.Ledger; +using Neo.SmartContract.Manifest; +using System; +using System.Linq; + +namespace Neo.SmartContract +{ + public class DeployedContract : Contract + { + public override UInt160 ScriptHash { get; } + + public DeployedContract(ContractState contract) + { + if (contract is null) throw new ArgumentNullException(nameof(contract)); + + Script = null; + ScriptHash = contract.ScriptHash; + ContractMethodDescriptor descriptor = contract.Manifest.Abi.GetMethod("verify"); + if (descriptor is null) throw new NotSupportedException("The smart contract haven't got verify method."); + + ParameterList = descriptor.Parameters.Select(u => u.Type).ToArray(); + } + } +} diff --git a/src/neo/SmartContract/Helper.cs b/src/neo/SmartContract/Helper.cs index 4448ff83b6..e89b34d158 100644 --- a/src/neo/SmartContract/Helper.cs +++ b/src/neo/SmartContract/Helper.cs @@ -147,6 +147,7 @@ internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snap for (int i = 0; i < hashes.Length; i++) { int offset; + ContractMethodDescriptor init = null; byte[] verification = verifiable.Witnesses[i].VerificationScript; if (verification.Length == 0) { @@ -156,6 +157,7 @@ internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snap if (md is null) return false; verification = cs.Script; offset = md.Offset; + init = cs.Manifest.Abi.GetMethod("_initialize"); } else { @@ -165,6 +167,7 @@ internal static bool VerifyWitnesses(this IVerifiable verifiable, StoreView snap using (ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, verifiable, snapshot.Clone(), gas)) { engine.LoadScript(verification, CallFlags.None).InstructionPointer = offset; + if (init != null) engine.LoadClonedContext(init.Offset); 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; diff --git a/src/neo/Wallets/Wallet.cs b/src/neo/Wallets/Wallet.cs index ddff297aaa..65c5505a6c 100644 --- a/src/neo/Wallets/Wallet.cs +++ b/src/neo/Wallets/Wallet.cs @@ -4,6 +4,7 @@ using Neo.Network.P2P.Payloads; using Neo.Persistence; using Neo.SmartContract; +using Neo.SmartContract.Manifest; using Neo.SmartContract.Native; using Neo.VM; using System; @@ -240,7 +241,7 @@ public virtual WalletAccount Import(string nep2, string passphrase, int N = 1638 return account; } - public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null) + public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null, Signer[] cosigners = null) { UInt160[] accounts; if (from is null) @@ -249,13 +250,11 @@ public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null } else { - if (!Contains(from)) - throw new ArgumentException($"The address {from.ToString()} was not found in the wallet"); accounts = new[] { from }; } using (SnapshotView snapshot = Blockchain.Singleton.GetSnapshot()) { - HashSet cosignerList = new HashSet(); + Dictionary cosignerList = cosigners?.ToDictionary(p => p.Account) ?? new Dictionary(); byte[] script; List<(UInt160 Account, BigInteger Value)> balances_gas = null; using (ScriptBuilder sb = new ScriptBuilder()) @@ -282,9 +281,21 @@ public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null { balances = balances.OrderBy(p => p.Value).ToList(); var balances_used = FindPayingAccounts(balances, output.Value.Value); - cosignerList.UnionWith(balances_used.Select(p => p.Account)); foreach (var (account, value) in balances_used) { + if (cosignerList.TryGetValue(account, out Signer signer)) + { + if (signer.Scopes != WitnessScope.Global) + signer.Scopes |= WitnessScope.CalledByEntry; + } + else + { + cosignerList.Add(account, new Signer + { + Account = account, + Scopes = WitnessScope.CalledByEntry + }); + } sb.EmitAppCall(output.AssetId, "transfer", account, output.ScriptHash, value); sb.Emit(OpCode.ASSERT); } @@ -297,14 +308,7 @@ public Transaction MakeTransaction(TransferOutput[] outputs, UInt160 from = null if (balances_gas is null) balances_gas = accounts.Select(p => (Account: p, Value: NativeContract.GAS.BalanceOf(snapshot, p))).Where(p => p.Value.Sign > 0).ToList(); - var cosigners = cosignerList.Select(p => new Signer() - { - // default access for transfers should be valid only for first invocation - Scopes = WitnessScope.CalledByEntry, - Account = p - }).ToArray(); - - return MakeTransaction(snapshot, script, cosigners, Array.Empty(), balances_gas); + return MakeTransaction(snapshot, script, cosignerList.Values.ToArray(), Array.Empty(), balances_gas); } } @@ -317,8 +321,6 @@ public Transaction MakeTransaction(byte[] script, UInt160 sender = null, Signer[ } else { - if (!Contains(sender)) - throw new ArgumentException($"The address {sender} was not found in the wallet"); accounts = new[] { sender }; } using (SnapshotView snapshot = Blockchain.Singleton.GetSnapshot()) @@ -353,49 +355,66 @@ private Transaction MakeTransaction(StoreView snapshot, byte[] script, Signer[] tx.SystemFee = engine.GasConsumed; } - UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot); - - // base size for transaction: includes const_header + signers + attributes + script + hashes - int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length); - - foreach (UInt160 hash in hashes) - { - byte[] witness_script = GetAccount(hash)?.Contract?.Script ?? snapshot.Contracts.TryGet(hash)?.Script; - if (witness_script is null) continue; - tx.NetworkFee += CalculateNetworkFee(witness_script, ref size); - } - tx.NetworkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); + tx.NetworkFee = CalculateNetworkFee(snapshot, tx); if (value >= tx.SystemFee + tx.NetworkFee) return tx; } throw new InvalidOperationException("Insufficient GAS"); } - public static long CalculateNetworkFee(byte[] witness_script, ref int size) + public long CalculateNetworkFee(StoreView snapshot, Transaction tx) { - long networkFee = 0; + UInt160[] hashes = tx.GetScriptHashesForVerifying(snapshot); - if (witness_script.IsSignatureContract()) - { - size += 67 + witness_script.GetVarSize(); - networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] + ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] + ApplicationEngine.OpCodePrices[OpCode.PUSHNULL] + ApplicationEngine.ECDsaVerifyPrice; - } - else if (witness_script.IsMultiSigContract(out int m, out int n)) - { - int size_inv = 66 * m; - size += IO.Helper.GetVarSize(size_inv) + size_inv + witness_script.GetVarSize(); - networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] * m; - using (ScriptBuilder sb = new ScriptBuilder()) - networkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(m).ToArray()[0]]; - networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] * n; - using (ScriptBuilder sb = new ScriptBuilder()) - networkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(n).ToArray()[0]]; - networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHNULL] + ApplicationEngine.ECDsaVerifyPrice * n; - } - else + // base size for transaction: includes const_header + signers + attributes + script + hashes + int size = Transaction.HeaderSize + tx.Signers.GetVarSize() + tx.Attributes.GetVarSize() + tx.Script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length); + long networkFee = 0; + foreach (UInt160 hash in hashes) { - //We can support more contract types in the future. + byte[] witness_script = GetAccount(hash)?.Contract?.Script; + if (witness_script is null) + { + var contract = snapshot.Contracts.TryGet(hash); + if (contract is null) continue; + + // Empty invocation and verification scripts + size += Array.Empty().GetVarSize() * 2; + + // Check verify cost + ContractMethodDescriptor verify = contract.Manifest.Abi.GetMethod("verify"); + if (verify is null) throw new ArgumentException($"The smart contract {contract.ScriptHash} haven't got verify method"); + ContractMethodDescriptor init = contract.Manifest.Abi.GetMethod("_initialize"); + using ApplicationEngine engine = ApplicationEngine.Create(TriggerType.Verification, tx, snapshot.Clone(), 0, testMode: true); + engine.LoadScript(contract.Script, CallFlags.None).InstructionPointer = verify.Offset; + if (init != null) engine.LoadClonedContext(init.Offset); + engine.LoadScript(Array.Empty(), CallFlags.None); + if (engine.Execute() == VMState.FAULT) throw new ArgumentException($"Smart contract {contract.ScriptHash} verification fault."); + if (engine.ResultStack.Count != 1 || !engine.ResultStack.Pop().GetBoolean()) throw new ArgumentException($"Smart contract {contract.ScriptHash} returns false."); + + networkFee += engine.GasConsumed; + } + else if (witness_script.IsSignatureContract()) + { + size += 67 + witness_script.GetVarSize(); + networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] + ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] + ApplicationEngine.OpCodePrices[OpCode.PUSHNULL] + ApplicationEngine.ECDsaVerifyPrice; + } + else if (witness_script.IsMultiSigContract(out int m, out int n)) + { + int size_inv = 66 * m; + size += IO.Helper.GetVarSize(size_inv) + size_inv + witness_script.GetVarSize(); + networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] * m; + using (ScriptBuilder sb = new ScriptBuilder()) + networkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(m).ToArray()[0]]; + networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHDATA1] * n; + using (ScriptBuilder sb = new ScriptBuilder()) + networkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(n).ToArray()[0]]; + networkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHNULL] + ApplicationEngine.ECDsaVerifyPrice * n; + } + else + { + //We can support more contract types in the future. + } } - + networkFee += size * NativeContract.Policy.GetFeePerByte(snapshot); return networkFee; } @@ -405,38 +424,56 @@ public bool Sign(ContractParametersContext context) foreach (UInt160 scriptHash in context.ScriptHashes) { WalletAccount account = GetAccount(scriptHash); - if (account is null) continue; - // Try to sign self-contained multiSig + if (account != null) + { + // Try to sign self-contained multiSig - Contract multiSigContract = account.Contract; + Contract multiSigContract = account.Contract; - if (multiSigContract != null && - multiSigContract.Script.IsMultiSigContract(out int m, out ECPoint[] points)) - { - foreach (var point in points) + if (multiSigContract != null && + multiSigContract.Script.IsMultiSigContract(out int m, out ECPoint[] points)) + { + foreach (var point in points) + { + account = GetAccount(point); + if (account?.HasKey != true) continue; + KeyPair key = account.GetKey(); + byte[] signature = context.Verifiable.Sign(key); + fSuccess |= context.AddSignature(multiSigContract, key.PublicKey, signature); + if (fSuccess) m--; + if (context.Completed || m <= 0) break; + } + continue; + } + else if (account.HasKey) { - account = GetAccount(point); - if (account?.HasKey != true) continue; + // Try to sign with regular accounts KeyPair key = account.GetKey(); byte[] signature = context.Verifiable.Sign(key); - fSuccess |= context.AddSignature(multiSigContract, key.PublicKey, signature); - if (fSuccess) m--; - if (context.Completed || m <= 0) break; + fSuccess |= context.AddSignature(account.Contract, key.PublicKey, signature); + continue; } } - else + + // Try Smart contract verification + + using var snapshot = Blockchain.Singleton.GetSnapshot(); + var contract = snapshot.Contracts.TryGet(scriptHash); + + if (contract != null) { - // Try to sign with regular accounts + var deployed = new DeployedContract(contract); - if (account.HasKey) + // Only works with verify without parameters + + if (deployed.ParameterList.Length == 0) { - KeyPair key = account.GetKey(); - byte[] signature = context.Verifiable.Sign(key); - fSuccess |= context.AddSignature(account.Contract, key.PublicKey, signature); + fSuccess |= context.Add(deployed); } } } + return fSuccess; } diff --git a/tests/neo.UnitTests/Extensions/Nep5NativeContractExtensions.cs b/tests/neo.UnitTests/Extensions/Nep5NativeContractExtensions.cs index b0cd686022..e6be60e661 100644 --- a/tests/neo.UnitTests/Extensions/Nep5NativeContractExtensions.cs +++ b/tests/neo.UnitTests/Extensions/Nep5NativeContractExtensions.cs @@ -4,7 +4,6 @@ using Neo.SmartContract; using Neo.SmartContract.Native; using Neo.VM; -using System; using System.IO; using System.Numerics; @@ -16,14 +15,10 @@ internal class ManualWitness : IVerifiable { private readonly UInt160[] _hashForVerify; - public Witness[] Witnesses - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - public int Size => 0; + public Witness[] Witnesses { get; set; } + public ManualWitness(params UInt160[] hashForVerify) { _hashForVerify = hashForVerify ?? new UInt160[0]; diff --git a/tests/neo.UnitTests/SmartContract/UT_InteropService.cs b/tests/neo.UnitTests/SmartContract/UT_InteropService.cs index 3a0062275f..c3764cc0fa 100644 --- a/tests/neo.UnitTests/SmartContract/UT_InteropService.cs +++ b/tests/neo.UnitTests/SmartContract/UT_InteropService.cs @@ -251,9 +251,13 @@ public void TestRuntime_CheckWitness() var engine = GetEngine(true); ((Transaction)engine.ScriptContainer).Signers[0].Account = Contract.CreateSignatureRedeemScript(pubkey).ToScriptHash(); + ((Transaction)engine.ScriptContainer).Signers[0].Scopes = WitnessScope.CalledByEntry; + engine.CheckWitness(pubkey.EncodePoint(true)).Should().BeTrue(); + engine.CheckWitness(((Transaction)engine.ScriptContainer).Sender.ToArray()).Should().BeTrue(); + + ((Transaction)engine.ScriptContainer).Signers = new Signer[0]; engine.CheckWitness(pubkey.EncodePoint(true)).Should().BeFalse(); - engine.CheckWitness(((Transaction)engine.ScriptContainer).Sender.ToArray()).Should().BeFalse(); Action action = () => engine.CheckWitness(new byte[0]); action.Should().Throw(); diff --git a/tests/neo.UnitTests/SmartContract/UT_SmartContractHelper.cs b/tests/neo.UnitTests/SmartContract/UT_SmartContractHelper.cs index 0f4e9f5f74..da6cf22d42 100644 --- a/tests/neo.UnitTests/SmartContract/UT_SmartContractHelper.cs +++ b/tests/neo.UnitTests/SmartContract/UT_SmartContractHelper.cs @@ -142,6 +142,21 @@ public void TestVerifyWitnesses() Manifest = TestUtils.CreateManifest(UInt160.Zero, "verify", ContractParameterType.Boolean, ContractParameterType.Signature), }); Assert.AreEqual(false, Neo.SmartContract.Helper.VerifyWitnesses(header3, snapshot3, 100)); + + // Smart contract verification + + var contract = new ContractState() + { + Script = "11".HexToBytes(), // 17 PUSH1 + Manifest = TestUtils.CreateManifest(UInt160.Zero, "verify", ContractParameterType.Boolean, ContractParameterType.Signature), // Offset = 0 + }; + snapshot3.Contracts.Add(contract.ScriptHash, contract); + var tx = new Extensions.Nep5NativeContractExtensions.ManualWitness(contract.ScriptHash) + { + Witnesses = new Witness[] { new Witness() { InvocationScript = new byte[0], VerificationScript = new byte[0] } } + }; + + Assert.AreEqual(true, Neo.SmartContract.Helper.VerifyWitnesses(tx, snapshot3, 1000)); } } } diff --git a/tests/neo.UnitTests/Wallets/UT_Wallet.cs b/tests/neo.UnitTests/Wallets/UT_Wallet.cs index 7f4ad69332..82b1031fbc 100644 --- a/tests/neo.UnitTests/Wallets/UT_Wallet.cs +++ b/tests/neo.UnitTests/Wallets/UT_Wallet.cs @@ -298,7 +298,7 @@ public void TestMakeTransaction1() Value = new BigDecimal(1,8) } }, UInt160.Zero); - action.Should().Throw(); + action.Should().Throw(); action = () => wallet.MakeTransaction(new TransferOutput[] {