From 6e0dcce270b766253d120489002119920929e98b Mon Sep 17 00:00:00 2001 From: quantumagi Date: Tue, 30 Mar 2021 16:46:05 +1100 Subject: [PATCH] System Dictionary (Candidate 1) --- .../SmartContracts/SystemDictionary.cs | 263 ++++++++++++++++++ .../Stratis.SmartContracts.CLR.Tests.csproj | 3 + .../SystemContractDictionaryTests.cs | 184 ++++++++++++ 3 files changed, 450 insertions(+) create mode 100644 src/Stratis.SmartContracts.CLR.Tests/SmartContracts/SystemDictionary.cs create mode 100644 src/Stratis.SmartContracts.CLR.Tests/SystemContractDictionaryTests.cs diff --git a/src/Stratis.SmartContracts.CLR.Tests/SmartContracts/SystemDictionary.cs b/src/Stratis.SmartContracts.CLR.Tests/SmartContracts/SystemDictionary.cs new file mode 100644 index 0000000000..fa5bd44a85 --- /dev/null +++ b/src/Stratis.SmartContracts.CLR.Tests/SmartContracts/SystemDictionary.cs @@ -0,0 +1,263 @@ +using Stratis.SmartContracts; + +public class SystemDictionary : SmartContract +{ + private const string errActionNotAnInitiator = "Not an initiator."; + private const string errActionInvalidatedDueToChanges = "The action has been invalidated due to parallel updates."; + private const string errActionInitiatorShouldCancel = "Only the action initiator can cancel it."; + private const string errActionDoesNotExist = "The action does not exist."; + + public enum ActionTypes + { + None, + WhiteListUpdate, + DictionaryUpdate + } + + public struct DictionaryEntry + { + public UInt256 CodeHash; + public Address Address; + } + + public enum Lookups + { + DictByName = 1, + NonceByHash = 2, + NonceByName = 3, + ActionDetails = 4, + WLByCodeHash = 5, + ApproversByAction = 6 + } + + public struct DictionaryUpdate + { + public string Name; + public UInt256 CodeHash; + public Address Address; + public uint Nonce; + public Address Owner; + public uint ExpiryHeight; + } + + public struct WhiteListUpdate + { + public UInt256 CodeHash; + public bool WhiteListed; + public uint Nonce; + public Address Owner; + public uint ExpiryHeight; + } + + private bool GetIsWhiteListed(UInt256 hash) => this.State.GetBool($"{Lookups.WLByCodeHash}:{hash}"); + private void SetIsWhiteListed(UInt256 hash, bool isWhiteListed) => this.State.SetStruct($"{Lookups.WLByCodeHash}:{hash}", isWhiteListed); + private DictionaryEntry GetDictionaryEntry(string name) => this.State.GetStruct($"{Lookups.DictByName}:{name}"); + private void SetDictionaryEntry(string name, DictionaryEntry value) => this.State.SetStruct($"{Lookups.DictByName}:{name}", value); + private uint GetNonce(UInt256 hash) => this.State.GetUInt32($"{Lookups.NonceByHash}:{hash}"); + private void SetNonce(UInt256 hash, uint nonce) => this.State.SetUInt32($"{Lookups.NonceByHash}:{hash}", nonce); + private uint GetNonce(string name) => this.State.GetUInt32($"{Lookups.NonceByName}:{name}"); + private void SetNonce(string name, uint nonce) => this.State.SetUInt32($"{Lookups.NonceByName}:{name}", nonce); + + private WhiteListUpdate GetWhiteListUpdate(string actionDescriptor) => this.State.GetStruct($"{Lookups.ActionDetails}:{actionDescriptor}"); + private void SetWhiteListUpdate(string actionDescriptor, WhiteListUpdate action) => this.State.SetStruct($"{Lookups.ActionDetails}:{actionDescriptor}", action); + private DictionaryUpdate GetDictionaryUpdate(string actionDescriptor) => this.State.GetStruct($"{Lookups.ActionDetails}:{actionDescriptor}"); + private void SetDictionaryUpdate(string actionDescriptor, DictionaryUpdate action) => this.State.SetStruct($"{Lookups.ActionDetails}:{actionDescriptor}", action); + private void ClearAction(string actionDescriptor) => this.State.Clear($"{Lookups.ActionDetails}:{actionDescriptor}"); + private Address[] GetApprovers(string actionDescriptor) => this.State.GetArray
($"{Lookups.ApproversByAction}:{actionDescriptor}"); + private void SetApprovers(string actionDescriptor, Address[] addresses) => this.State.SetArray($"{Lookups.ApproversByAction}:{actionDescriptor}", addresses); + private void ClearApprovers(string actionDescriptor) => this.State.Clear($"{Lookups.ApproversByAction}:{actionDescriptor}"); + + public SystemDictionary(ISmartContractState state) : base(state) + { + } + + public bool IsWhiteListed(UInt256 codeHash) + { + Assert(codeHash != default(UInt256)); + + return this.GetIsWhiteListed(codeHash); + } + + public UInt256 GetCodeHash(string name) + { + Assert(!string.IsNullOrEmpty(name)); + + return GetDictionaryEntry(name).CodeHash; + } + + public Address GetContractAddress(string name) + { + Assert(!string.IsNullOrEmpty(name)); + + return GetDictionaryEntry(name).Address; + } + + public string UpdateDictionary(string name, UInt256 codeHash, Address address, uint expiryHeight) + { + Assert(!string.IsNullOrEmpty(name)); + //Assert(IsInitiator(nameof(DictionaryUpdate), this.Message.Sender), errActionNotAnInitiator); + Assert(GetIsWhiteListed(codeHash)); + Assert(expiryHeight > this.Block.Number); + + DictionaryUpdate dictionaryUpdate; + + dictionaryUpdate.Name = name; + dictionaryUpdate.CodeHash = codeHash; + dictionaryUpdate.Address = address; + dictionaryUpdate.Nonce = GetNonce(name); + dictionaryUpdate.Owner = this.Message.Sender; + dictionaryUpdate.ExpiryHeight = expiryHeight; + + string actionDescriptor = DictionaryUpdateActionDescriptor(dictionaryUpdate); + + SetDictionaryUpdate(actionDescriptor, dictionaryUpdate); + + return actionDescriptor; + } + + private string DictionaryUpdateActionDescriptor(DictionaryUpdate dictionaryUpdate) + { + return $"{nameof(ActionTypes.DictionaryUpdate)}(Name:{dictionaryUpdate.Name},CodeHash:{dictionaryUpdate.CodeHash},Address:{dictionaryUpdate.Address},Nonce:{dictionaryUpdate.Nonce})"; + } + + private string WhiteListUpdateActionDescriptor(WhiteListUpdate whiteListUpdate) + { + return $"{nameof(ActionTypes.WhiteListUpdate)}(CodeHash:{whiteListUpdate.CodeHash},WhiteListed:{whiteListUpdate.WhiteListed},Nonce:{whiteListUpdate.Nonce},Owner:{whiteListUpdate.Owner})"; + } + + public string UpdateWhiteList(UInt256 codeHash, bool whiteListed, uint expiryHeight) + { + Assert(codeHash != default(UInt256)); + //Assert(IsInitiator(nameof(WhiteListUpdate), this.Message.Sender), errActionNotAnInitiator); + Assert(!GetIsWhiteListed(codeHash)); + Assert(expiryHeight > this.Block.Number); + + WhiteListUpdate whiteListUpdate; + + whiteListUpdate.CodeHash = codeHash; + whiteListUpdate.WhiteListed = whiteListed; + whiteListUpdate.Nonce = GetNonce(whiteListUpdate.CodeHash); + whiteListUpdate.Owner = this.Message.Sender; + whiteListUpdate.ExpiryHeight = expiryHeight; + + string actionDescriptor = WhiteListUpdateActionDescriptor(whiteListUpdate); + + SetWhiteListUpdate(actionDescriptor, whiteListUpdate); + + return actionDescriptor; + } + + public string ApproveWhiteListUpdate(string actionDescriptor) + { + Assert(!string.IsNullOrEmpty(actionDescriptor)); + + WhiteListUpdate whiteListUpdate = GetWhiteListUpdate(actionDescriptor); + + Assert(whiteListUpdate.CodeHash != default(UInt256), errActionDoesNotExist); + + if (whiteListUpdate.ExpiryHeight <= this.Block.Number) + { + ClearAction(actionDescriptor); + ClearApprovers(actionDescriptor); + Assert(false, errActionDoesNotExist); + } + + string approvalStatus = Approve(actionDescriptor); + if (IsApprovedStatus(approvalStatus)) + { + SetIsWhiteListed(whiteListUpdate.CodeHash, whiteListUpdate.WhiteListed); + SetNonce(whiteListUpdate.CodeHash, whiteListUpdate.Nonce + 1); + + ClearAction(actionDescriptor); + ClearApprovers(actionDescriptor); + } + + return approvalStatus; + } + + public string ApproveDictionaryUpdate(string actionDescriptor) + { + Assert(!string.IsNullOrEmpty(actionDescriptor)); + + DictionaryUpdate dictionaryUpdate = GetDictionaryUpdate(actionDescriptor); + + Assert(!string.IsNullOrEmpty(dictionaryUpdate.Name), errActionDoesNotExist); + + if (dictionaryUpdate.ExpiryHeight <= this.Block.Number) + { + ClearAction(actionDescriptor); + ClearApprovers(actionDescriptor); + Assert(false, errActionDoesNotExist); + } + + string approvalStatus = Approve(actionDescriptor); + if (IsApprovedStatus(approvalStatus)) + { + Assert(GetNonce(dictionaryUpdate.Name) == dictionaryUpdate.Nonce, errActionInvalidatedDueToChanges); + + SetDictionaryEntry(dictionaryUpdate.Name, new DictionaryEntry() { Address = dictionaryUpdate.Address, CodeHash = dictionaryUpdate.CodeHash }); + SetNonce(dictionaryUpdate.Name, dictionaryUpdate.Nonce + 1); + + ClearAction(actionDescriptor); + ClearApprovers(actionDescriptor); + } + + return approvalStatus; + } + + public void CancelWhiteListUpdate(string actionDescriptor) + { + Assert(!string.IsNullOrEmpty(actionDescriptor)); + + WhiteListUpdate whiteListUpdate = GetWhiteListUpdate(actionDescriptor); + + Assert(whiteListUpdate.CodeHash != default(UInt256), errActionDoesNotExist); + Assert(whiteListUpdate.Owner == this.Message.Sender, errActionInitiatorShouldCancel); + + ClearAction(actionDescriptor); + ClearApprovers(actionDescriptor); + } + + public void CancelDictionaryUpdate(string actionDescriptor) + { + Assert(!string.IsNullOrEmpty(actionDescriptor)); + + DictionaryUpdate dictionaryUpdate = GetDictionaryUpdate(actionDescriptor); + + Assert(!string.IsNullOrEmpty(dictionaryUpdate.Name), errActionDoesNotExist); + Assert(dictionaryUpdate.Owner == this.Message.Sender, errActionInitiatorShouldCancel); + + ClearAction(actionDescriptor); + ClearApprovers(actionDescriptor); + } + + private bool IsApprovedStatus(string approvalStatus) => approvalStatus.StartsWith("Approved"); + + private string Approve(string actionDescriptor) + { + // Assert(IsApprover(action.GetType().Name, this.Message.Sender), "Not an approver."); + + bool alreadyApproved = false; + Address[] approvers = GetApprovers(actionDescriptor); + for (int i = 0; i < approvers.Length; i++) + { + if (approvers[i] == this.Message.Sender) + { + alreadyApproved = true; + break; + } + } + + if (!alreadyApproved) + { + Address[] newApprovers = new Address[approvers.Length + 1]; + newApprovers[0] = this.Message.Sender; + System.Array.Copy(approvers, 0, newApprovers, 1, approvers.Length); + SetApprovers(actionDescriptor, newApprovers); + } + + string approvalStatus = ""; // ApprovalStatus(action.GetType().Name, approvers) + + return approvalStatus; + } +} \ No newline at end of file diff --git a/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj b/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj index 6a4b6b4f7d..01479681e7 100644 --- a/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj +++ b/src/Stratis.SmartContracts.CLR.Tests/Stratis.SmartContracts.CLR.Tests.csproj @@ -104,6 +104,9 @@ Always + + PreserveNewest + Always diff --git a/src/Stratis.SmartContracts.CLR.Tests/SystemContractDictionaryTests.cs b/src/Stratis.SmartContracts.CLR.Tests/SystemContractDictionaryTests.cs new file mode 100644 index 0000000000..50219b9bae --- /dev/null +++ b/src/Stratis.SmartContracts.CLR.Tests/SystemContractDictionaryTests.cs @@ -0,0 +1,184 @@ +using System; +using Microsoft.Extensions.Logging; +using Moq; +using NBitcoin; +using Stratis.Bitcoin.Configuration.Logging; +using Stratis.Patricia; +using Stratis.SmartContracts.CLR.Caching; +using Stratis.SmartContracts.CLR.Compilation; +using Stratis.SmartContracts.CLR.Loader; +using Stratis.SmartContracts.CLR.ResultProcessors; +using Stratis.SmartContracts.CLR.Serialization; +using Stratis.SmartContracts.CLR.Validation; +using Stratis.SmartContracts.Core; +using Stratis.SmartContracts.Core.State; +using Stratis.SmartContracts.Networks; +using Xunit; + + +namespace Stratis.SmartContracts.CLR.Tests +{ + public class SystemContractDictionaryTests + { + private const ulong BlockHeight = 0; + private static readonly uint160 CoinbaseAddress = 0; + private static readonly uint160 ToAddress = 1; + private static readonly uint160 SenderAddress = 2; + private static readonly Money MempoolFee = new Money(1_000_000); + private readonly IKeyEncodingStrategy keyEncodingStrategy; + private readonly ILoggerFactory loggerFactory; + private readonly Network network; + private readonly IContractRefundProcessor refundProcessor; + private readonly IStateRepositoryRoot state; + private readonly IContractTransferProcessor transferProcessor; + private readonly SmartContractValidator validator; + private IInternalExecutorFactory internalTxExecutorFactory; + private readonly IContractAssemblyCache contractCache; + private IVirtualMachine vm; + private readonly ICallDataSerializer callDataSerializer; + private readonly StateFactory stateFactory; + private readonly IAddressGenerator addressGenerator; + private readonly ILoader assemblyLoader; + private readonly IContractModuleDefinitionReader moduleDefinitionReader; + private readonly IContractPrimitiveSerializer contractPrimitiveSerializer; + private readonly IStateProcessor stateProcessor; + private readonly ISmartContractStateFactory smartContractStateFactory; + private readonly ISerializer serializer; + + public SystemContractDictionaryTests() + { + this.keyEncodingStrategy = BasicKeyEncodingStrategy.Default; + this.loggerFactory = ExtendedLoggerFactory.Create(); + this.network = new SmartContractsRegTest(); + this.refundProcessor = new ContractRefundProcessor(this.loggerFactory); + this.state = new StateRepositoryRoot(new NoDeleteSource(new MemoryDictionarySource())); + this.transferProcessor = new ContractTransferProcessor(this.loggerFactory, this.network); + this.validator = new SmartContractValidator(); + this.addressGenerator = new AddressGenerator(); + this.assemblyLoader = new ContractAssemblyLoader(); + this.moduleDefinitionReader = new ContractModuleDefinitionReader(); + this.contractPrimitiveSerializer = new ContractPrimitiveSerializer(this.network); + this.serializer = new Serializer(this.contractPrimitiveSerializer); + this.contractCache = new ContractAssemblyCache(); + this.vm = new ReflectionVirtualMachine(this.validator, this.loggerFactory, this.assemblyLoader, this.moduleDefinitionReader, this.contractCache); + this.stateProcessor = new StateProcessor(this.vm, this.addressGenerator); + this.internalTxExecutorFactory = new InternalExecutorFactory(this.loggerFactory, this.stateProcessor); + this.smartContractStateFactory = new SmartContractStateFactory(this.contractPrimitiveSerializer, this.internalTxExecutorFactory, this.serializer); + + this.callDataSerializer = new CallDataSerializer(this.contractPrimitiveSerializer); + + this.stateFactory = new StateFactory(this.smartContractStateFactory); + } + + [Fact] + public void CanCompileSystemDictionary() + { + ContractCompilationResult compilationResult = ContractCompiler.CompileFile("SmartContracts/SystemDictionary.cs"); + Assert.True(compilationResult.Success); + byte[] contractCode = compilationResult.Compilation; + + var contractTxData = new ContractTxData(0, (RuntimeObserver.Gas)1, (RuntimeObserver.Gas)500_000, contractCode); + var tx = new Transaction(); + tx.AddOutput(0, new Script(this.callDataSerializer.Serialize(contractTxData))); + + IContractTransactionContext transactionContext = new ContractTransactionContext(BlockHeight, CoinbaseAddress, MempoolFee, new uint160(2), tx); + + var executor = new ContractExecutor( + this.callDataSerializer, + this.state, + this.refundProcessor, + this.transferProcessor, + this.stateFactory, + this.stateProcessor, + this.contractPrimitiveSerializer); + + IContractExecutionResult result = executor.Execute(transactionContext); + + Assert.Null(result.ErrorMessage); + } + + [Fact] + public void CanWhitelistSystemContracts() + { + var internalTxExecutor = new Mock(); + var internalHashHelper = new Mock(); + var persistentState = new TestPersistentState(); + var block = new Mock(); + var message = new Mock(); + Func getBalance = () => 1; + + ISmartContractState state = Mock.Of( + g => g.InternalTransactionExecutor == internalTxExecutor.Object + && g.InternalHashHelper == internalHashHelper.Object + && g.PersistentState == persistentState + && g.Block == block.Object + && g.Message == message.Object + && g.GetBalance == getBalance); + + IContract contract = Contract.CreateUninitialized(typeof(SystemDictionary), state, new uint160(2)); + var instance = (SystemDictionary)contract.GetPrivateFieldValue("instance"); + + // TODO: Verify signatures. + UInt256 codeHash = 1; + Address address = new Address(0, 0, 0, 0, 1); + + var callWhiteList = new MethodCall("UpdateWhiteList", new object[] { codeHash, true, (uint)100 }); + IContractInvocationResult resultWhiteList = contract.Invoke(callWhiteList); + Assert.True(resultWhiteList.IsSuccess); + + var whiteListUpdate = new SystemDictionary.WhiteListUpdate() { CodeHash = codeHash, WhiteListed = true, ExpiryHeight = 100, Owner = Address.Zero, Nonce = 0 }; + string action = $"{SystemDictionary.ActionTypes.WhiteListUpdate}(CodeHash:{whiteListUpdate.CodeHash},WhiteListed:{whiteListUpdate.WhiteListed},Nonce:{whiteListUpdate.Nonce},Owner:{whiteListUpdate.Owner})"; + var whiteListUpdateConf = persistentState.GetStruct($"{SystemDictionary.Lookups.ActionDetails}:{action}"); + + Assert.Equal(whiteListUpdate.CodeHash, whiteListUpdateConf.CodeHash); + Assert.Equal(whiteListUpdate.WhiteListed, whiteListUpdateConf.WhiteListed); + Assert.Equal(whiteListUpdate.ExpiryHeight, whiteListUpdateConf.ExpiryHeight); + Assert.Equal(whiteListUpdate.Nonce, whiteListUpdateConf.Nonce); + + + /* + Assert.Equal(codeHash, persistentState.GetUInt256($"ByName:{name}")); + + var callIsWhiteListed = new MethodCall("IsWhiteListed", new object[] { codeHash }); + IContractInvocationResult resultIsWhiteListed = contract.Invoke(callIsWhiteListed); + Assert.True((bool)resultIsWhiteListed.Return); + + var callGetCodeHash = new MethodCall("GetCodeHash", new object[] { name }); + IContractInvocationResult resultGetCodeHash = contract.Invoke(callGetCodeHash); + Assert.Equal(codeHash, (UInt256)resultGetCodeHash.Return); + + var callGetContractAddress = new MethodCall("GetContractAddress", new object[] { name }); + IContractInvocationResult resultGetContractAddress = contract.Invoke(callGetContractAddress); + Assert.Equal(address, (Address)resultGetContractAddress.Return); + + var callGetContractAddressCH = new MethodCall("GetContractAddress", new object[] { codeHash }); + IContractInvocationResult resultGetContractAddressCH = contract.Invoke(callGetContractAddressCH); + Assert.Equal(address, (Address)resultGetContractAddressCH.Return); + + var callBlackList = new MethodCall("BlackList", new object[] { signatures, codeHash }); + IContractInvocationResult resultBlackList = contract.Invoke(callBlackList); + Assert.True(resultBlackList.IsSuccess); + + var callIsWhiteListed2 = new MethodCall("IsWhiteListed", new object[] { codeHash }); + IContractInvocationResult resultIsWhiteListed2 = contract.Invoke(callIsWhiteListed2); + Assert.False((bool)resultIsWhiteListed2.Return); + + Assert.Equal(codeHash, persistentState.GetUInt256($"ByName:{name}")); + + // TODO: Should these methods return anything once the conteact is black-listed? + + var callGetCodeHash2 = new MethodCall("GetCodeHash", new object[] { name }); + IContractInvocationResult resultGetCodeHash2 = contract.Invoke(callGetCodeHash2); + Assert.Equal(codeHash, (UInt256)resultGetCodeHash2.Return); + + var callGetContractAddress2 = new MethodCall("GetContractAddress", new object[] { name }); + IContractInvocationResult resultGetContractAddress2 = contract.Invoke(callGetContractAddress2); + Assert.Equal(address, (Address)resultGetContractAddress2.Return); + + var callGetContractAddressCH2 = new MethodCall("GetContractAddress", new object[] { codeHash }); + IContractInvocationResult resultGetContractAddressCH2 = contract.Invoke(callGetContractAddressCH2); + Assert.Equal(address, (Address)resultGetContractAddressCH2.Return); + */ + } + } +} \ No newline at end of file