Skip to content

Commit

Permalink
Allow smart contract verification (neo-project#1800)
Browse files Browse the repository at this point in the history
* Allow sc verify

* Create DeployedContract

* Fix fee

* Fix

* Change to exceptions

* Allow static variables during verification

* Simplify DeployedContract

* Change verify call flags

* Change to ReadOnly

* Add error description

* Allow cosigners in makeTransaction

* Verify as APPCALL

* Fix fee

* Revert "Fix fee"

This reverts commit e8d59a9.

* Revert "Verify as APPCALL"

This reverts commit d8bf588.

* Auto stash before revert of "Allow cosigners in makeTransaction"

* None

* Add comment

* Fix CustomGroups

* Fix UT

* Update DeployedContract.cs

* Optimize MakeTransaction()

* Update Wallet.cs

* Fix

* Fix fee calculation

* Optimize CalculateNetworkFee()

* Try sc verification when the account it's null

* Update Wallet.cs

* Update Wallet.cs

* Add true verify test

* Update Wallet.cs

* Remove check account in wallet

* Fix UT

Co-authored-by: erikzhang <erik@neo.org>
  • Loading branch information
2 people authored and cloud8little committed Jan 24, 2021
1 parent d257a95 commit feb2a8e
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 79 deletions.
14 changes: 12 additions & 2 deletions src/neo/SmartContract/ApplicationEngine.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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))
Expand All @@ -119,6 +119,11 @@ internal bool CheckWitnessInternal(UInt160 hash)
}
if (signer.Scopes.HasFlag(WitnessScope.CustomGroups))
{
// Check allow state callflag

if (!CurrentContext.GetState<ExecutionContextState>().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())
Expand All @@ -127,6 +132,11 @@ internal bool CheckWitnessInternal(UInt160 hash)
return false;
}

// Check allow state callflag

if (!CurrentContext.GetState<ExecutionContextState>().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);
Expand Down
1 change: 0 additions & 1 deletion src/neo/SmartContract/ContractParametersContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Neo.SmartContract
{
Expand Down
24 changes: 24 additions & 0 deletions src/neo/SmartContract/DeployedContract.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
3 changes: 3 additions & 0 deletions src/neo/SmartContract/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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
{
Expand All @@ -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;
Expand Down
171 changes: 104 additions & 67 deletions src/neo/Wallets/Wallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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<UInt160> cosignerList = new HashSet<UInt160>();
Dictionary<UInt160, Signer> cosignerList = cosigners?.ToDictionary(p => p.Account) ?? new Dictionary<UInt160, Signer>();
byte[] script;
List<(UInt160 Account, BigInteger Value)> balances_gas = null;
using (ScriptBuilder sb = new ScriptBuilder())
Expand All @@ -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);
}
Expand All @@ -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<TransactionAttribute>(), balances_gas);
return MakeTransaction(snapshot, script, cosignerList.Values.ToArray(), Array.Empty<TransactionAttribute>(), balances_gas);
}
}

Expand All @@ -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())
Expand Down Expand Up @@ -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<byte>().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<byte>(), 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;
}

Expand All @@ -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;
}

Expand Down
Loading

0 comments on commit feb2a8e

Please sign in to comment.