Skip to content
16 changes: 16 additions & 0 deletions src/Neo/SmartContract/ApplicationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ protected static void OnSysCall(ExecutionEngine engine, Instruction instruction)
/// <param name="datoshi">The amount of GAS, in the unit of datoshi, 1 datoshi = 1e-8 GAS, to be added.</param>
protected internal void AddFee(long datoshi)
{
// Check whitelist

if (CurrentContext?.GetState<ExecutionContextState>()?.WhiteListed == true)
{
// The execution is whitelisted
return;
}

FeeConsumed = checked(FeeConsumed + datoshi);
if (FeeConsumed > _feeAmount)
throw new InvalidOperationException("Insufficient GAS.");
Expand Down Expand Up @@ -320,6 +328,14 @@ private ExecutionContext CallContractInternal(ContractState contract, ContractMe
throw new InvalidOperationException($"Cannot Call Method {method.Name} Of Contract {contract.Hash} From Contract {CurrentScriptHash}");
}

// Check whitelist

if (NativeContract.Policy.IsWhitelistFeeContract(SnapshotCache, contract.Hash, method.Name, method.Parameters.Length, out var fixedFee))
{
AddFee(fixedFee.Value);
state.WhiteListed = true;
}

if (invocationCounter.TryGetValue(contract.Hash, out var counter))
{
invocationCounter[contract.Hash] = counter + 1;
Expand Down
5 changes: 5 additions & 0 deletions src/Neo/SmartContract/ExecutionContextState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ public class ExecutionContextState
public int NotificationCount { get; set; }

public bool IsDynamicCall { get; set; }

/// <summary>
/// True if the execution is whitelisted by committee
/// </summary>
public bool WhiteListed { get; set; }
}
7 changes: 6 additions & 1 deletion src/Neo/SmartContract/Native/ContractManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,10 @@ private ContractTask Update(ApplicationEngine engine, byte[]? nefFile, byte[]? m
contract.Manifest = manifestNew;
}
Helper.Check(new Script(contract.Nef.Script, true), contract.Manifest.Abi);
contract.UpdateCounter++; // Increase update counter
// Increase update counter
contract.UpdateCounter++;
// Clean whitelist (emit event if exists)
Policy.CleanWhitelist(engine, contract.Hash);
return OnDeployAsync(engine, contract, data, true);
}

Expand All @@ -375,6 +378,8 @@ private void Destroy(ApplicationEngine engine)
engine.SnapshotCache.Delete(key);
// lock contract
Policy.BlockAccount(engine.SnapshotCache, hash);
// Clean whitelist (emit event if exists)
Policy.CleanWhitelist(engine, contract.Hash);
// emit event
engine.SendNotification(Hash, "Destroy", new Array(engine.ReferenceCounter) { hash.ToArray() });
}
Expand Down
11 changes: 9 additions & 2 deletions src/Neo/SmartContract/Native/NativeContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ protected static void AssertCommittee(ApplicationEngine engine)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt256 hash, UInt160 signer) => StorageKey.Create(Id, prefix, hash, signer);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected StorageKey CreateStorageKey(byte prefix, UInt160 hash, string methodName, int bigEndianKey) => StorageKey.Create(Id, prefix, hash, methodName, bigEndianKey);

#endregion

/// <summary>
Expand Down Expand Up @@ -432,8 +435,12 @@ internal async void Invoke(ApplicationEngine engine, byte version)
var state = context.GetState<ExecutionContextState>();
if (!state.CallFlags.HasFlag(method.RequiredCallFlags))
throw new InvalidOperationException($"Cannot call this method with the flag {state.CallFlags}.");
// In the unit of datoshi, 1 datoshi = 1e-8 GAS
engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice);
// Check native-whitelist
if (!Policy.IsWhitelistFeeContract(engine.SnapshotCache, Hash, method.Name, method.Parameters.Length, out var fixedFee))
{
// In the unit of datoshi, 1 datoshi = 1e-8 GAS
engine.AddFee(method.CpuFee * engine.ExecFeeFactor + method.StorageFee * engine.StoragePrice);
}
List<object?> parameters = new();
if (method.NeedApplicationEngine) parameters.Add(engine);
if (method.NeedSnapshot) parameters.Add(engine.SnapshotCache);
Expand Down
128 changes: 128 additions & 0 deletions src/Neo/SmartContract/Native/PolicyContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

#pragma warning disable IDE0051

using Neo.Extensions.IO;
using Neo.Network.P2P.Payloads;
using Neo.Persistence;
using Neo.SmartContract.Iterators;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;

namespace Neo.SmartContract.Native;
Expand All @@ -25,6 +27,12 @@ namespace Neo.SmartContract.Native;
"old", ContractParameterType.Integer,
"new", ContractParameterType.Integer
)]
[ContractEvent(1, name: WhitelistChangedEventName,
"contract", ContractParameterType.Hash160,
"method", ContractParameterType.String,
"argCount", ContractParameterType.Integer,
"fee", ContractParameterType.Any
)]
public sealed class PolicyContract : NativeContract
{
/// <summary>
Expand Down Expand Up @@ -86,6 +94,7 @@ public sealed class PolicyContract : NativeContract
public const uint MaxMaxTraceableBlocks = 2102400;

private const byte Prefix_BlockedAccount = 15;
private const byte Prefix_WhitelistedFeeContracts = 16;
private const byte Prefix_FeePerByte = 10;
private const byte Prefix_ExecFeeFactor = 18;
private const byte Prefix_StoragePrice = 19;
Expand All @@ -105,6 +114,7 @@ public sealed class PolicyContract : NativeContract
/// The event name for the block generation time changed.
/// </summary>
private const string MillisecondsPerBlockChangedEventName = "MillisecondsPerBlockChanged";
private const string WhitelistChangedEventName = "WhitelistFeeChanged";

internal PolicyContract()
{
Expand Down Expand Up @@ -366,6 +376,124 @@ private bool UnblockAccount(ApplicationEngine engine, UInt160 account)
return true;
}

internal bool IsWhitelistFeeContract(DataCache snapshot, UInt160 contractHash, string method, int argCount, [NotNullWhen(true)] out long? fixedFee)
{
// Check contract existence

var currentContract = ContractManagement.GetContract(snapshot, contractHash);

if (currentContract != null)
{
// Check state existence

var item = snapshot.TryGet(CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount));

if (item != null)
{
fixedFee = (long)(BigInteger)item;
return true;
}
}

fixedFee = null;
return false;
}

/// <summary>
/// Remove whitelisted Fee contracts
/// </summary>
/// <param name="engine">The execution engine.</param>
/// <param name="contractHash">The contract to set the whitelist</param>
/// <param name="method">Method</param>
/// <param name="argCount">Argument count</param>
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
private void RemoveWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount)
{
if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature");

var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount);

if (!engine.SnapshotCache.Contains(key)) throw new InvalidOperationException("Whitelist not found");

engine.SnapshotCache.Delete(key);

// Emit event
engine.SendNotification(Hash, WhitelistChangedEventName,
[new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()),
new VM.Types.Integer(argCount), VM.Types.StackItem.Null]);
}

internal int CleanWhitelist(ApplicationEngine engine, UInt160 contractHash)
{
var count = 0;
var searchKey = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash);

foreach ((var key, _) in engine.SnapshotCache.Find(searchKey, SeekDirection.Forward))
{
engine.SnapshotCache.Delete(key);
count++;

// Emit event recovering the values from the Key

var keyData = key.ToArray().AsSpan();
(var method, var argCount) = StorageKey.ReadMethodAndArgCount(key.ToArray().AsSpan());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erikzhang We need a method for unwrap a StorageKey

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an idea. Instead of storing the methodName and argCount directly, we can read the contract's ABI and find the method's offset.

public class ContractMethodDescriptor : ContractEventDescriptor, IEquatable<ContractMethodDescriptor>
{
/// <summary>
/// Indicates the return type of the method. It can be any value of <see cref="ContractParameterType"/>.
/// </summary>
public ContractParameterType ReturnType { get; set; }
/// <summary>
/// The position of the method in the contract script.
/// </summary>
public int Offset { get; set; }

Once the contract is updated, all of its whitelists will become invalid. Therefore, storing only the Offset is safe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will safe storage, it's a good idea


engine.SendNotification(Hash, WhitelistChangedEventName,
[new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()),
new VM.Types.Integer(argCount), VM.Types.StackItem.Null]);
}

return count;
}

/// <summary>
/// Set whitelisted Fee contracts
/// </summary>
/// <param name="engine">The execution engine.</param>
/// <param name="contractHash">The contract to set the whitelist</param>
/// <param name="method">Method</param>
/// <param name="argCount">Argument count</param>
/// <param name="fixedFee">Fixed execution fee</param>
[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States | CallFlags.AllowNotify)]
internal void SetWhitelistFeeContract(ApplicationEngine engine, UInt160 contractHash, string method, int argCount, long fixedFee)
{
ArgumentOutOfRangeException.ThrowIfNegative(fixedFee, nameof(fixedFee));

if (!CheckCommittee(engine)) throw new InvalidOperationException("Invalid committee signature");

var key = CreateStorageKey(Prefix_WhitelistedFeeContracts, contractHash, method, argCount);

// Validate methods
var contract = ContractManagement.GetContract(engine.SnapshotCache, contractHash)
?? throw new InvalidOperationException("Is not a valid contract");

if (contract.Manifest.Abi.GetMethod(method, argCount) is null)
throw new InvalidOperationException($"{method} with {argCount} args is not a valid method of {contractHash}");

// Set
var entry = engine.SnapshotCache
.GetAndChange(key, () => new StorageItem(fixedFee));

entry.Set(fixedFee);

// Emit event

engine.SendNotification(Hash, WhitelistChangedEventName,
[new VM.Types.ByteString(contractHash.ToArray()), new VM.Types.ByteString(method.ToStrictUtf8Bytes()),
new VM.Types.Integer(argCount), new VM.Types.Integer(fixedFee)]);
}

[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
internal StorageIterator GetWhitelistFeeContracts(DataCache snapshot)
{
const FindOptions options = FindOptions.RemovePrefix | FindOptions.KeysOnly;
var enumerator = snapshot
.Find(CreateStorageKey(Prefix_WhitelistedFeeContracts), SeekDirection.Forward)
.GetEnumerator();

return new StorageIterator(enumerator, 1, options);
}

[ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)]
private StorageIterator GetBlockedAccounts(DataCache snapshot)
{
Expand Down
31 changes: 31 additions & 0 deletions src/Neo/SmartContract/StorageKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,37 @@ public static StorageKey Create(int id, byte prefix, ISerializableSpan content)
return Create(id, prefix, content.GetSpan());
}

/// <summary>
/// Create StorageKey
/// </summary>
/// <param name="id">The id of the contract.</param>
/// <param name="prefix">The prefix of the key.</param>
/// <param name="hash">Hash</param>
/// <param name="methodName">Method Name</param>
/// <param name="bigEndian">Big Endian key.</param>
/// <returns>The <see cref="StorageKey"/> class</returns>
public static StorageKey Create(int id, byte prefix, UInt160 hash, string methodName, int bigEndian)
{
const int HashAndInt = UInt160Length + sizeof(int);

var methodData = methodName.ToStrictUtf8Bytes();
var data = new byte[HashAndInt + methodData.Length];

FillHeader(data, id, prefix);
hash.Serialize(data.AsSpan(PrefixLength..));
BinaryPrimitives.WriteInt32BigEndian(data.AsSpan(UInt160Length..), bigEndian);
Array.Copy(methodData, 0, data, HashAndInt, methodData.Length);

return new(id, data);
}

internal static (string methodName, int bigEndian) ReadMethodAndArgCount(ReadOnlySpan<byte> keyData)
{
var argCount = BinaryPrimitives.ReadInt32BigEndian(keyData.Slice(UInt160Length, 4));
var method = keyData[(UInt160Length + 4)..];
return (method.ToStrictUtf8String(), argCount);
}

/// <summary>
/// Creates a search prefix for a contract.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions tests/Neo.UnitTests/Ledger/UT_StorageKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

using Neo.Cryptography.ECC;
using Neo.SmartContract;
using System.Text;

namespace Neo.UnitTests.Ledger;

Expand Down Expand Up @@ -73,6 +74,23 @@ public void SameTest()
UInt256.Parse("0x761a9bb72ca2a63984db0cc43f943a2a25e464f62d1a91114c2b6fbbfd24b51d"),
UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302")).ToArray());

// UInt160+String+Int
key = new KeyBuilder(1, 2);
key.Add(UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302"));
key.AddBigEndian((int)3); // arg count
key.Add(Encoding.UTF8.GetBytes("hello world"));

CollectionAssert.AreEqual(key.ToArray(), StorageKey.Create(1, 2,
UInt160.Parse("2d3b96ae1bcc5a585e075e3b81920210dec16302"), "hello world", 3).ToArray());

// Recover method and arg count

var keyB = new StorageKey(key.ToArray()).ToArray().AsSpan();
(var method, var argCount) = StorageKey.ReadMethodAndArgCount(keyB);

Assert.AreEqual(3, argCount);
Assert.AreEqual("hello world", method);

// ISerializable
key = new KeyBuilder(1, 2);
key.Add(ECCurve.Secp256r1.G);
Expand Down
Loading