diff --git a/src/neo/Ledger/Blockchain.cs b/src/neo/Ledger/Blockchain.cs index 4836e7da89..be01d352be 100644 --- a/src/neo/Ledger/Blockchain.cs +++ b/src/neo/Ledger/Blockchain.cs @@ -225,6 +225,7 @@ private void OnInventory(IInventory inventory, bool relay = true) Block block => OnNewBlock(block), Transaction transaction => OnNewTransaction(transaction), ExtensiblePayload payload => OnNewExtensiblePayload(payload), + NotaryRequest payload => OnNotaryRequest(payload), _ => throw new NotSupportedException() }; if (result == VerifyResult.Succeed && relay) @@ -329,6 +330,13 @@ private VerifyResult OnNewExtensiblePayload(ExtensiblePayload payload) return VerifyResult.Succeed; } + private VerifyResult OnNotaryRequest(NotaryRequest payload) + { + if (!payload.Verify(system.Settings)) return VerifyResult.Invalid; + system.RelayCache.Add(payload); + return VerifyResult.Succeed; + } + private VerifyResult OnNewTransaction(Transaction transaction) { if (system.ContainsTransaction(transaction.Hash)) return VerifyResult.AlreadyExists; diff --git a/src/neo/Ledger/MemoryPool.cs b/src/neo/Ledger/MemoryPool.cs index 426c7f1fd8..f7b7ba5d23 100644 --- a/src/neo/Ledger/MemoryPool.cs +++ b/src/neo/Ledger/MemoryPool.cs @@ -13,6 +13,7 @@ using Neo.Network.P2P.Payloads; using Neo.Persistence; using Neo.Plugins; +using Neo.Wallets; using System; using System.Collections; using System.Collections.Generic; @@ -293,6 +294,7 @@ internal VerifyResult TryAdd(Transaction tx, DataCache snapshot) { VerifyResult result = tx.VerifyStateDependent(_system.Settings, snapshot, VerificationContext); if (result != VerifyResult.Succeed) return result; + if (!CheckConflicts(tx)) return VerifyResult.Invalid; _unsortedTransactions.Add(tx.Hash, poolItem); VerificationContext.AddTransaction(tx); @@ -317,6 +319,36 @@ internal VerifyResult TryAdd(Transaction tx, DataCache snapshot) return VerifyResult.Succeed; } + private bool CheckConflicts(Transaction tx) + { + List to_removed = new(); + foreach (var hash in tx.GetAttributes().Select(p => p.Hash)) + { + if (_unsortedTransactions.TryGetValue(hash, out PoolItem item)) + { + if (!tx.Signers.Select(p => p.Account).Contains(item.Tx.Sender)) return false; + if (tx.NetworkFee < item.Tx.NetworkFee) return false; + to_removed.Add(item); + } + } + foreach (var item in _sortedTransactions) + { + var conflicts = item.Tx.GetAttributes().Select(p => p.Hash); + if (conflicts.Contains(tx.Hash)) + { + if (item.Tx.Signers.Select(p => p.Account).Contains(tx.Sender) && tx.NetworkFee < item.Tx.NetworkFee) return false; + to_removed.Add(item); + } + } + foreach (var item in to_removed) + { + _unsortedTransactions.Remove(item.Tx.Hash); + _sortedTransactions.Remove(item); + VerificationContext.RemoveTransaction(item.Tx); + } + return true; + } + private List RemoveOverCapacity() { List removedTransactions = new(); diff --git a/src/neo/Network/P2P/MessageCommand.cs b/src/neo/Network/P2P/MessageCommand.cs index 52dcac236a..a44747a4c6 100644 --- a/src/neo/Network/P2P/MessageCommand.cs +++ b/src/neo/Network/P2P/MessageCommand.cs @@ -122,6 +122,12 @@ public enum MessageCommand : byte [ReflectionCache(typeof(Block))] Block = 0x2c, + /// + /// Sent to send an . + /// + [ReflectionCache(typeof(NotaryRequest))] + Notary = 0x2d, + /// /// Sent to send an . /// diff --git a/src/neo/Network/P2P/Payloads/ConflictAttribute.cs b/src/neo/Network/P2P/Payloads/ConflictAttribute.cs new file mode 100644 index 0000000000..d71f1402b5 --- /dev/null +++ b/src/neo/Network/P2P/Payloads/ConflictAttribute.cs @@ -0,0 +1,45 @@ +using Neo.IO; +using Neo.IO.Json; +using Neo.Persistence; +using Neo.SmartContract.Native; +using System.IO; + +namespace Neo.Network.P2P.Payloads +{ + /// + /// Indicates that the transaction is conflict with another. + /// + public class ConflictAttribute : TransactionAttribute + { + /// + /// Indicates the conflict transaction hash. + /// + public UInt256 Hash; + + public override TransactionAttributeType Type => TransactionAttributeType.Conflict; + + public override bool AllowMultiple => true; + + protected override void DeserializeWithoutType(BinaryReader reader) + { + Hash = reader.ReadSerializable(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Hash); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["hash"] = Hash.ToString(); + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + return NativeContract.Ledger.ContainsTransaction(snapshot, Hash); + } + } +} diff --git a/src/neo/Network/P2P/Payloads/InventoryType.cs b/src/neo/Network/P2P/Payloads/InventoryType.cs index f30bf679a6..7e77b723e4 100644 --- a/src/neo/Network/P2P/Payloads/InventoryType.cs +++ b/src/neo/Network/P2P/Payloads/InventoryType.cs @@ -28,6 +28,11 @@ public enum InventoryType : byte /// /// Indicates that the inventory is an . /// - Extensible = MessageCommand.Extensible + Extensible = MessageCommand.Extensible, + + /// + /// Indicates that the inventory is an . + /// + Notary = MessageCommand.Notary } } diff --git a/src/neo/Network/P2P/Payloads/NotValidBefore.cs b/src/neo/Network/P2P/Payloads/NotValidBefore.cs new file mode 100644 index 0000000000..631ba4f583 --- /dev/null +++ b/src/neo/Network/P2P/Payloads/NotValidBefore.cs @@ -0,0 +1,48 @@ +using Neo.IO.Json; +using Neo.Persistence; +using Neo.SmartContract.Native; +using System.IO; + +namespace Neo.Network.P2P.Payloads +{ + /// + /// Indicates that the transaction is not valid before specified height. + /// + public class NotValidBefore : TransactionAttribute + { + /// + /// Indicates that the transaction is not valid before this height. + /// + public uint Height; + + public override TransactionAttributeType Type => TransactionAttributeType.NotValidBefore; + + public override bool AllowMultiple => false; + + protected override void DeserializeWithoutType(BinaryReader reader) + { + Height = reader.ReadUInt32(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(Height); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["height"] = Height; + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + var maxNVBDelta = NativeContract.Notary.GetMaxNotValidBeforeDelta(snapshot); + var block_height = NativeContract.Ledger.CurrentIndex(snapshot); + if (block_height < Height) return false; + if ((block_height + maxNVBDelta) < Height) return false; + return tx.ValidUntilBlock <= (Height + maxNVBDelta); + } + } +} diff --git a/src/neo/Network/P2P/Payloads/NotaryAssisted.cs b/src/neo/Network/P2P/Payloads/NotaryAssisted.cs new file mode 100644 index 0000000000..8cd1202834 --- /dev/null +++ b/src/neo/Network/P2P/Payloads/NotaryAssisted.cs @@ -0,0 +1,45 @@ +using Neo.IO.Json; +using Neo.Persistence; +using Neo.SmartContract.Native; +using System.IO; +using System.Linq; + +namespace Neo.Network.P2P.Payloads +{ + /// + /// Indicates that the transaction is an Notrary tx. + /// + public class NotaryAssisted : TransactionAttribute + { + /// + /// Indicates how many signatures the Notary need to collect. + /// + public byte NKeys; + + public override TransactionAttributeType Type => TransactionAttributeType.NotaryAssisted; + + public override bool AllowMultiple => false; + + protected override void DeserializeWithoutType(BinaryReader reader) + { + NKeys = reader.ReadByte(); + } + + protected override void SerializeWithoutType(BinaryWriter writer) + { + writer.Write(NKeys); + } + + public override JObject ToJson() + { + JObject json = base.ToJson(); + json["nkeys"] = NKeys; + return json; + } + + public override bool Verify(DataCache snapshot, Transaction tx) + { + return tx.Signers.First(p => p.Account.Equals(NativeContract.Notary.Hash)) is not null; + } + } +} diff --git a/src/neo/Network/P2P/Payloads/NotaryRequest.cs b/src/neo/Network/P2P/Payloads/NotaryRequest.cs new file mode 100644 index 0000000000..aea52a8f3c --- /dev/null +++ b/src/neo/Network/P2P/Payloads/NotaryRequest.cs @@ -0,0 +1,138 @@ +using Akka.Actor; +using Neo.Cryptography; +using Neo.IO; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.VM; +using System; +using System.IO; +using System.Linq; + +namespace Neo.Network.P2P.Payloads +{ + public class NotaryRequest : IInventory + { + /// + /// Represents the fixed value of the field of the fallback transaction. + /// + public static readonly byte[] FallbackFixedScript = new byte[] { (byte)OpCode.RET }; + + /// + /// The transaction need Notary to collect signatures. + /// + private Transaction mainTransaction; + + /// + /// This transaction is valid when MainTransaction failed. + /// + private Transaction fallbackTransaction; + + /// + /// The witness of the payload. It must be one of multi-sig address of . + /// + private Witness witness; + private UInt256 hash = null; + + public InventoryType InventoryType => InventoryType.Notary; + + public UInt256 Hash + { + get + { + if (hash == null) + { + hash = this.CalculateHash(); + } + return hash; + } + } + + public Witness[] Witnesses + { + get + { + return new Witness[] { witness }; + } + set + { + witness = value[0]; + } + } + + public Transaction MainTransaction + { + get => mainTransaction; + set + { + mainTransaction = value; + hash = null; + } + } + + public Transaction FallbackTransaction + { + get => fallbackTransaction; + set + { + fallbackTransaction = value; + hash = null; + } + } + + public int Size => mainTransaction.Size + fallbackTransaction.Size + witness.Size; + + + public void DeserializeUnsigned(BinaryReader reader) + { + mainTransaction = reader.ReadSerializable(); + fallbackTransaction = reader.ReadSerializable(); + } + + public void Deserialize(BinaryReader reader) + { + DeserializeUnsigned(reader); + witness = reader.ReadSerializable(); + } + + public void Serialize(BinaryWriter writer) + { + SerializeUnsigned(writer); + writer.Write(witness); + } + + public void SerializeUnsigned(BinaryWriter writer) + { + writer.Write(mainTransaction); + writer.Write(fallbackTransaction); + } + + public UInt160[] GetScriptHashesForVerifying(DataCache snapshot) + { + return new UInt160[] { fallbackTransaction.Signers[1].Account }; + } + + public bool Verify(ProtocolSettings settings) + { + var nKeysMain = MainTransaction.GetAttributes(); + if (!nKeysMain.Any()) return false; + if (nKeysMain.ToArray()[0].NKeys == 0) return false; + if (!fallbackTransaction.Script.SequenceEqual(FallbackFixedScript)) return false; + if (FallbackTransaction.Signers.Length != 2) return false; + if (fallbackTransaction.Signers[1].Scopes != WitnessScope.None) return false; + if (FallbackTransaction.Witnesses[0].InvocationScript.Length != 66 + || FallbackTransaction.Witnesses[0].VerificationScript.Length != 0 + || (FallbackTransaction.Witnesses[0].InvocationScript[0] != (byte)OpCode.PUSHDATA1 && FallbackTransaction.Witnesses[0].InvocationScript[1] != 64)) + return false; + if (FallbackTransaction.GetAttribute() is null) return false; + var conflicts = FallbackTransaction.GetAttributes(); + if (conflicts.Count() != 1) return false; + if (conflicts.ToArray()[0].Hash != MainTransaction.Hash) return false; + var nKeysFallback = FallbackTransaction.GetAttributes(); + if (!nKeysFallback.Any()) return false; + if (nKeysFallback.ToArray()[0].NKeys != 0) return false; + if (MainTransaction.ValidUntilBlock != FallbackTransaction.ValidUntilBlock) return false; + if (!fallbackTransaction.VerifyWitness(settings, null, fallbackTransaction.Signers[1].Account, fallbackTransaction.Witnesses[1], 0_02000000, out _)) return false; + return this.VerifyWitnesses(settings, null, 0_02000000); + } + } +} diff --git a/src/neo/Network/P2P/Payloads/Transaction.cs b/src/neo/Network/P2P/Payloads/Transaction.cs index e8249fb6e7..4800efea8d 100644 --- a/src/neo/Network/P2P/Payloads/Transaction.cs +++ b/src/neo/Network/P2P/Payloads/Transaction.cs @@ -337,12 +337,13 @@ public JObject ToJson(ProtocolSettings settings) /// The used to verify the transaction. /// The snapshot used to verify the transaction. /// The used to verify the transaction. + /// The transactions used to verify the conflict. /// The result of the verification. - public VerifyResult Verify(ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context) + public VerifyResult Verify(ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context, IEnumerable mempool = null) { VerifyResult result = VerifyStateIndependent(settings); if (result != VerifyResult.Succeed) return result; - return VerifyStateDependent(settings, snapshot, context); + return VerifyStateDependent(settings, snapshot, context, mempool); } /// @@ -351,8 +352,9 @@ public VerifyResult Verify(ProtocolSettings settings, DataCache snapshot, Transa /// The used to verify the transaction. /// The snapshot used to verify the transaction. /// The used to verify the transaction. + /// The transactions used to verify the conflict. /// The result of the verification. - public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context) + public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context, IEnumerable mempool = null) { uint height = NativeContract.Ledger.CurrentIndex(snapshot); if (ValidUntilBlock <= height || ValidUntilBlock > height + settings.MaxValidUntilBlockIncrement) @@ -362,10 +364,16 @@ public virtual VerifyResult VerifyStateDependent(ProtocolSettings settings, Data if (NativeContract.Policy.IsBlocked(snapshot, hash)) return VerifyResult.PolicyFail; if (!(context?.CheckTransaction(this, snapshot) ?? true)) return VerifyResult.InsufficientFunds; + long notary_fee = 0; foreach (TransactionAttribute attribute in Attributes) + { if (!attribute.Verify(snapshot, this)) return VerifyResult.InvalidAttribute; - long net_fee = NetworkFee - Size * NativeContract.Policy.GetFeePerByte(snapshot); + if (attribute is NotaryAssisted) + notary_fee = (((NotaryAssisted)attribute).NKeys + 1) * NativeContract.Notary.GetNotaryServiceFeePerKey(snapshot); + } + if (mempool is not null && !VerifyConflicts(mempool)) return VerifyResult.Invalid; + long net_fee = NetworkFee - Size * NativeContract.Policy.GetFeePerByte(snapshot) - notary_fee; if (net_fee < 0) return VerifyResult.InsufficientFunds; if (net_fee > MaxVerificationGas) net_fee = MaxVerificationGas; @@ -449,6 +457,20 @@ public virtual VerifyResult VerifyStateIndependent(ProtocolSettings settings) return VerifyResult.Succeed; } + private bool VerifyConflicts(IEnumerable mempool) + { + return !GetAttributes() + .Select(p => p.Hash) + .Intersect(mempool.Select(p => p.Hash)) + .Any() && + !mempool + .Select(p => p + .GetAttributes() + .Select(p => p.Hash)) + .Aggregate(Enumerable.Empty(), (conflicts, p) => conflicts.Union(p)) + .Contains(Hash); + } + public StackItem ToStackItem(ReferenceCounter referenceCounter) { return new Array(referenceCounter, new StackItem[] diff --git a/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs b/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs index fae5a09ee2..a72f5f3d1d 100644 --- a/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs +++ b/src/neo/Network/P2P/Payloads/TransactionAttributeType.cs @@ -27,6 +27,24 @@ public enum TransactionAttributeType : byte /// Indicates that the transaction is an oracle response. /// [ReflectionCache(typeof(OracleResponse))] - OracleResponse = 0x11 + OracleResponse = 0x11, + + /// + /// Indicates that the transaction is not valid before . + /// + [ReflectionCache(typeof(NotValidBefore))] + NotValidBefore = 0xe0, + + /// + /// Indicates that the transaction is conflict with . + /// + [ReflectionCache(typeof(ConflictAttribute))] + Conflict = 0xe1, + + /// + /// Indicates that the transaction need Notarys to collect signatures. + /// + [ReflectionCache(typeof(NotaryAssisted))] + NotaryAssisted = 0xe2 } } diff --git a/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs b/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs index ad73c50456..0c411da5c2 100644 --- a/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs +++ b/src/neo/Network/P2P/RemoteNode.ProtocolHandler.cs @@ -72,6 +72,7 @@ private void OnMessage(Message msg) break; case MessageCommand.Block: case MessageCommand.Extensible: + case MessageCommand.Notary: OnInventoryReceived((IInventory)msg.Payload); break; case MessageCommand.FilterAdd: diff --git a/src/neo/ProtocolSettings.cs b/src/neo/ProtocolSettings.cs index 83d1f9c474..ee7ffe9b8f 100644 --- a/src/neo/ProtocolSettings.cs +++ b/src/neo/ProtocolSettings.cs @@ -156,7 +156,8 @@ public record ProtocolSettings [nameof(GasToken)] = new[] { 0u }, [nameof(PolicyContract)] = new[] { 0u }, [nameof(RoleManagement)] = new[] { 0u }, - [nameof(OracleContract)] = new[] { 0u } + [nameof(OracleContract)] = new[] { 0u }, + [nameof(NotaryContract)] = new[] { 0u } } }; diff --git a/src/neo/SmartContract/Native/LedgerContract.cs b/src/neo/SmartContract/Native/LedgerContract.cs index c3900709d6..f1356294f5 100644 --- a/src/neo/SmartContract/Native/LedgerContract.cs +++ b/src/neo/SmartContract/Native/LedgerContract.cs @@ -29,6 +29,7 @@ public sealed class LedgerContract : NativeContract private const byte Prefix_CurrentBlock = 12; private const byte Prefix_Block = 5; private const byte Prefix_Transaction = 11; + private const byte Prefix_TrimmedTransaction = 3; internal LedgerContract() { @@ -46,7 +47,16 @@ internal override ContractTask OnPersist(ApplicationEngine engine) engine.Snapshot.Add(CreateStorageKey(Prefix_Block).Add(engine.PersistingBlock.Hash), new StorageItem(Trim(engine.PersistingBlock).ToArray())); foreach (TransactionState tx in transactions) { - engine.Snapshot.Add(CreateStorageKey(Prefix_Transaction).Add(tx.Transaction.Hash), new StorageItem(tx)); + engine.Snapshot.Add(CreateStorageKey(Prefix_Transaction).Add(tx.Transaction.Hash), new StorageItem(new TransactionState + { + BlockIndex = engine.PersistingBlock.Index, + Transaction = tx.Transaction + })); + foreach (var attr in tx.Transaction.GetAttributes()) + { + var hash = ((ConflictAttribute)attr).Hash; + engine.Snapshot.Add(CreateStorageKey(Prefix_TrimmedTransaction).Add(hash), new StorageItem(engine.PersistingBlock.Index)); + } } engine.SetState(transactions); return ContractTask.CompletedTask; diff --git a/src/neo/SmartContract/Native/NativeContract.cs b/src/neo/SmartContract/Native/NativeContract.cs index b19e5fe349..452528029f 100644 --- a/src/neo/SmartContract/Native/NativeContract.cs +++ b/src/neo/SmartContract/Native/NativeContract.cs @@ -75,6 +75,11 @@ public abstract class NativeContract /// public static OracleContract Oracle { get; } = new(); + /// + /// Gets the instance of the class. + /// + public static NotaryContract Notary { get; } = new(); + #endregion /// diff --git a/src/neo/SmartContract/Native/NotaryContract.cs b/src/neo/SmartContract/Native/NotaryContract.cs new file mode 100644 index 0000000000..3a80077f10 --- /dev/null +++ b/src/neo/SmartContract/Native/NotaryContract.cs @@ -0,0 +1,324 @@ +using Neo.Cryptography; +using Neo.Cryptography.ECC; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.VM; +using Neo.VM.Types; +using System; +using System.Linq; +using System.Numerics; +using Array = Neo.VM.Types.Array; + +namespace Neo.SmartContract.Native +{ + /// + /// The native Notary service for NEO system. + /// + public sealed class NotaryContract : NativeContract + { + private const long DefaultNotaryServiceFeePerKey = 1000_0000; + private const int DefaultDepositDeltaTill = 5760; + private const int DefaultMaxNotValidBeforeDelta = 140; + + private const byte PrefixDeposit = 0x01; + private const byte PreMaxNotValidBeforeDelta = 0x10; + private const byte PreNotaryServiceFeePerKey = 0x05; + + internal NotaryContract() + { + } + + internal override ContractTask Initialize(ApplicationEngine engine) + { + engine.Snapshot.Add(CreateStorageKey(PreMaxNotValidBeforeDelta), new StorageItem(DefaultMaxNotValidBeforeDelta)); + engine.Snapshot.Add(CreateStorageKey(PreNotaryServiceFeePerKey), new StorageItem(DefaultNotaryServiceFeePerKey)); + return ContractTask.CompletedTask; + } + + internal override async ContractTask OnPersist(ApplicationEngine engine) + { + long nFees = 0; + ECPoint[] notaries = null; + foreach (Transaction tx in engine.PersistingBlock.Transactions) + { + if (tx.GetAttribute() is not null) + { + if (notaries is null) notaries = GetNotaryNodes(engine.Snapshot); + } + var nKeys = tx.GetAttributes().ToArray()[0].NKeys; + nFees = (long)nKeys + 1; + if (tx.Sender == Notary.Hash) + { + var payer = tx.Signers[1]; + var balance = GetDepositFor(engine.Snapshot, payer.Account); + balance.amount -= tx.SystemFee + tx.NetworkFee; + if (balance.amount.Sign == 0) RemoveDepositFor(engine.Snapshot, payer.Account); + else PutDepositFor(engine, payer.Account, balance); + } + } + if (nFees == 0) return; + var singleReward = CalculateNotaryReward(engine.Snapshot, nFees, notaries.Length); + foreach (var notary in notaries) await GAS.Mint(engine, notary.EncodePoint(true).ToScriptHash(), singleReward, false); + } + + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private StackItem OnNEP17Payment(ApplicationEngine engine, UInt160 from, BigInteger amount, StackItem data) + { + if (engine.CallingScriptHash != GAS.Hash) throw new Exception(string.Format("only GAS can be accepted for deposit, got {0}", engine.CallingScriptHash.ToString())); + var to = from; + var additionalParams = (Array)data; + if (additionalParams.Count != 2) throw new Exception("`data` parameter should be an array of 2 elements"); + if (!additionalParams[0].Equals(StackItem.Null)) to = additionalParams[0].GetSpan().AsSerializable(); + var tx = engine.GetScriptContainer().GetSpan().AsSerializable(); + var allowedChangeTill = tx.Sender == to; + var currentHeight = Ledger.CurrentIndex(engine.Snapshot); + Deposit deposit = GetDepositFor(engine.Snapshot, to); + var till = (uint)additionalParams[1].GetInteger(); + if (till < currentHeight) throw new Exception(string.Format("`till` shouldn't be less then the chain's height {0}", currentHeight)); + if (deposit != null && till < deposit.till) throw new Exception(string.Format("`till` shouldn't be less then the previous value {0}", deposit.till)); + if (deposit is null) + { + if (amount.CompareTo(2 * GetNotaryServiceFeePerKey(engine.Snapshot)) < 0) throw new Exception(string.Format("first deposit can not be less then {0}, got {1}", 2 * GetNotaryServiceFeePerKey(engine.Snapshot), amount)); + deposit = new Deposit() { amount = 0, till = 0 }; + if (!allowedChangeTill) till = currentHeight + DefaultDepositDeltaTill; + } + else if (!allowedChangeTill) till = deposit.till; + deposit.amount += amount; + deposit.till = till; + PutDepositFor(engine, to, deposit); + return StackItem.Null; + } + + /// + /// Lock asset until the specified height is unlocked + /// + /// ApplicationEngine + /// Account + /// specified height + /// result + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private bool LockDepositUntil(ApplicationEngine engine, UInt160 addr, uint till) + { + if (engine.CheckWitnessInternal(addr)) return false; + if (till < Ledger.CurrentIndex(engine.Snapshot)) return false; + Deposit deposit = GetDepositFor(engine.Snapshot, addr); + if (deposit is null) return false; + if (till < deposit.till) return false; + deposit.till = till; + PutDepositFor(engine, addr, deposit); + return true; + } + + /// + /// Withdraw sends all deposited GAS for "from" address to "to" address. + /// + /// ApplicationEngine + /// From Account + /// To Account + /// void + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private async ContractTask Withdraw(ApplicationEngine engine, UInt160 from, UInt160 to) + { + if (engine.CheckWitnessInternal(from)) throw new InvalidOperationException(string.Format("Failed to check witness for {0}", from.ToString())); + Deposit deposit = GetDepositFor(engine.Snapshot, from); + if (deposit is null) throw new InvalidOperationException(string.Format("Deposit of {0} is null", from.ToString())); + if (Ledger.CurrentIndex(engine.Snapshot) < deposit.till) throw new InvalidOperationException(string.Format("Can't withdraw before {0}", deposit.till)); + await GAS.Burn(engine, from, deposit.amount); + await GAS.Mint(engine, to, deposit.amount, true); + RemoveDepositFor(engine.Snapshot, from); + } + + /// + /// BalanceOf returns deposited GAS amount for specified address. + /// + /// DataCache + /// Account + /// + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public BigInteger BalanceOf(DataCache snapshot, UInt160 acc) + { + Deposit deposit = GetDepositFor(snapshot, acc); + if (deposit is null) return 0; + return deposit.amount; + } + + /// + /// ExpirationOf Returns deposit lock height for specified address. + /// + /// DataCache + /// Account + /// + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint ExpirationOf(DataCache snapshot, UInt160 acc) + { + Deposit deposit = GetDepositFor(snapshot, acc); + if (deposit is null) return 0; + return deposit.till; + } + + /// + /// Verify checks whether the transaction was signed by one of the notaries. + /// + /// ApplicationEngine + /// Signature + /// + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + private bool Verify(ApplicationEngine engine, byte[] sig) + { + Transaction tx = (Transaction)engine.ScriptContainer; + if (tx.GetAttribute() is null) return false; + foreach (var signer in tx.Signers) + { + if (signer.Account == Notary.Hash) + { + if (signer.Scopes != WitnessScope.None) return false; + break; + } + } + if (tx.Sender == Notary.Hash) + { + if (tx.Signers.Length != 2) return false; + var payer = tx.Signers[1].Account; + var balance = GetDepositFor(engine.Snapshot, payer); + if (balance is null || balance.amount.CompareTo((tx.NetworkFee + tx.SystemFee)) < 0) return false; + } + ECPoint[] notaries = GetNotaryNodes(engine.Snapshot); + var hash = tx.Hash.ToArray(); + var verified = false; + foreach (var n in notaries) + { + if (Crypto.VerifySignature(hash, sig, n)) + { + verified = true; + break; + } + } + return verified; + } + + /// + /// GetNotaryNodes returns public keys of notary nodes. + /// + /// DataCache + /// + private ECPoint[] GetNotaryNodes(DataCache snapshot) + { + ECPoint[] nodes = RoleManagement.GetDesignatedByRole(snapshot, Role.Notary, uint.MaxValue); + return nodes; + } + + /// + /// GetMaxNotValidBeforeDelta is Notary contract method and returns the maximum NotValidBefore delta. + /// + /// DataCache + /// NotValidBefore + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public uint GetMaxNotValidBeforeDelta(DataCache snapshot) + { + return (uint)(BigInteger)snapshot[CreateStorageKey(PreMaxNotValidBeforeDelta)]; + } + + /// + /// SetMaxNotValidBeforeDelta is Notary contract method and sets the maximum NotValidBefore delta. + /// + /// ApplicationEngine + /// Value + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetMaxNotValidBeforeDelta(ApplicationEngine engine, uint value) + { + if (value > engine.ProtocolSettings.MaxValidUntilBlockIncrement / 2 || value < ProtocolSettings.Default.ValidatorsCount) throw new FormatException(string.Format("MaxNotValidBeforeDelta cannot be more than {0} or less than {1}", engine.ProtocolSettings.MaxValidUntilBlockIncrement / 2, ProtocolSettings.Default.ValidatorsCount)); + if (!CheckCommittee(engine)) throw new InvalidOperationException(); + engine.Snapshot.GetAndChange(CreateStorageKey(PreMaxNotValidBeforeDelta)).Set(value); + } + + /// + /// GetNotaryServiceFeePerKey is Notary contract method and returns the NotaryServiceFeePerKey delta. + /// + /// DataCache + /// NotaryServiceFeePerKey + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.ReadStates)] + public long GetNotaryServiceFeePerKey(DataCache snapshot) + { + return (long)(BigInteger)snapshot[CreateStorageKey(PreNotaryServiceFeePerKey)]; + } + + /// + /// SetNotaryServiceFeePerKey is Notary contract method and sets the NotaryServiceFeePerKey delta. + /// + /// ApplicationEngine + /// value + [ContractMethod(CpuFee = 1 << 15, RequiredCallFlags = CallFlags.States)] + private void SetNotaryServiceFeePerKey(ApplicationEngine engine, long value) + { + if (value < 0) throw new FormatException("NotaryServiceFeePerKey cannot be less than 0"); + if (!CheckCommittee(engine)) throw new InvalidOperationException(); + engine.Snapshot.GetAndChange(CreateStorageKey(PreNotaryServiceFeePerKey)).Set(value); + } + + /// + /// GetDepositFor returns state.Deposit for the account specified. It returns nil in case if + /// deposit is not found in storage and panics in case of any other error. + /// + /// + /// + /// + private Deposit GetDepositFor(DataCache snapshot, UInt160 acc) + { + Deposit deposit = snapshot.TryGet(CreateStorageKey(PrefixDeposit).Add(acc.ToArray()))?.GetInteroperable(); + if (deposit is null) throw new Exception(string.Format("failed to get deposit for {0} from storage", acc.ToString())); + return deposit; + } + + /// + /// PutDepositFor puts deposit on the balance of the specified account in the storage. + /// + /// ApplicationEngine + /// Account + /// deposit + private void PutDepositFor(ApplicationEngine engine, UInt160 acc, Deposit deposit) + { + engine.Snapshot.Add(CreateStorageKey(PrefixDeposit).Add(acc.ToArray()), new StorageItem(deposit)); + } + + /// + /// RemoveDepositFor removes deposit from the storage. + /// + /// DataCache + /// Account + private void RemoveDepositFor(DataCache snapshot, UInt160 acc) + { + snapshot.Delete(CreateStorageKey(PrefixDeposit).Add(acc.ToArray())); + } + + /// + /// CalculateNotaryReward calculates the reward for a single notary node based on FEE's count and Notary nodes count. + /// + /// DataCache + /// + /// + /// result + private long CalculateNotaryReward(DataCache snapshot, long nFees, int notariesCount) + { + return nFees * GetNotaryServiceFeePerKey(snapshot) / notariesCount; + } + + public class Deposit : IInteroperable + { + public BigInteger amount; + public uint till; + + public void FromStackItem(StackItem stackItem) + { + Struct @struct = (Struct)stackItem; + amount = @struct[0].GetInteger(); + till = (uint)@struct[1].GetInteger(); + } + + public StackItem ToStackItem(ReferenceCounter referenceCounter) + { + return new Struct(referenceCounter) { amount, till }; + } + } + } +} diff --git a/src/neo/SmartContract/Native/Role.cs b/src/neo/SmartContract/Native/Role.cs index 0c764d1a43..0a75cdb97d 100644 --- a/src/neo/SmartContract/Native/Role.cs +++ b/src/neo/SmartContract/Native/Role.cs @@ -28,6 +28,11 @@ public enum Role : byte /// /// NeoFS Alphabet nodes. /// - NeoFSAlphabetNode = 16 + NeoFSAlphabetNode = 16, + + /// + /// Notary nodes + /// + Notary = 128 } } diff --git a/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs b/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs index f76b022176..52701273cd 100644 --- a/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs +++ b/tests/neo.UnitTests/Ledger/UT_MemoryPool.cs @@ -87,7 +87,7 @@ private Transaction CreateTransactionWithFee(long fee) var randomBytes = new byte[16]; random.NextBytes(randomBytes); Mock mock = new(); - mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny(), null)).Returns(VerifyResult.Succeed); mock.Setup(p => p.VerifyStateIndependent(It.IsAny())).Returns(VerifyResult.Succeed); mock.Object.Script = randomBytes; mock.Object.NetworkFee = fee; @@ -111,7 +111,7 @@ private Transaction CreateTransactionWithFeeAndBalanceVerify(long fee) random.NextBytes(randomBytes); Mock mock = new(); UInt160 sender = senderAccount; - mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny())).Returns((ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context) => context.CheckTransaction(mock.Object, snapshot) ? VerifyResult.Succeed : VerifyResult.InsufficientFunds); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny(), null)).Returns((ProtocolSettings settings, DataCache snapshot, TransactionVerificationContext context, IEnumerable mempool) => context.CheckTransaction(mock.Object, snapshot) ? VerifyResult.Succeed : VerifyResult.InsufficientFunds); mock.Setup(p => p.VerifyStateIndependent(It.IsAny())).Returns(VerifyResult.Succeed); mock.Object.Script = randomBytes; mock.Object.NetworkFee = fee; diff --git a/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs b/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs index 27d584dca9..634ef09b18 100644 --- a/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs +++ b/tests/neo.UnitTests/Ledger/UT_TransactionVerificationContext.cs @@ -27,7 +27,7 @@ private Transaction CreateTransactionWithFee(long networkFee, long systemFee) var randomBytes = new byte[16]; random.NextBytes(randomBytes); Mock mock = new(); - mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny())).Returns(VerifyResult.Succeed); + mock.Setup(p => p.VerifyStateDependent(It.IsAny(), It.IsAny(), It.IsAny(), null)).Returns(VerifyResult.Succeed); mock.Setup(p => p.VerifyStateIndependent(It.IsAny())).Returns(VerifyResult.Succeed); mock.Object.Script = randomBytes; mock.Object.NetworkFee = networkFee; diff --git a/tests/neo.UnitTests/SmartContract/Native/UT_Notary.cs b/tests/neo.UnitTests/SmartContract/Native/UT_Notary.cs new file mode 100644 index 0000000000..3108840d6c --- /dev/null +++ b/tests/neo.UnitTests/SmartContract/Native/UT_Notary.cs @@ -0,0 +1,103 @@ +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.IO; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.SmartContract; +using Neo.SmartContract.Native; +using Neo.UnitTests.Extensions; +using Neo.VM; +using System; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +namespace Neo.UnitTests.SmartContract.Native +{ + [TestClass] + public class UT_Notary + { + private DataCache _snapshot; + + [TestInitialize] + public void TestSetup() + { + _snapshot = TestBlockchain.GetTestSnapshot(); + } + + [TestMethod] + public void Check_BalanceOf() + { + Assert.ThrowsException(() => NativeContract.Notary.BalanceOf(_snapshot, UInt160.Zero)); + } + + [TestMethod] + public void Check_ExpirationOf() + { + Assert.ThrowsException(() => NativeContract.Notary.ExpirationOf(_snapshot, UInt160.Zero)); + } + + [TestMethod] + public void Check_GetMaxNotValidBeforeDelta() + { + NativeContract.Notary.GetMaxNotValidBeforeDelta(_snapshot).Should().Be(140); + } + + [TestMethod] + public void Check_SetMaxNotValidBeforeDelta() + { + var snapshot = _snapshot.CreateSnapshot(); + // Fake blockchain + var persistingBlock = new Block { Header = new Header { Index = 1000 } }; + UInt160 committeeAddress = NativeContract.NEO.GetCommitteeAddress(snapshot); + + using var engine = ApplicationEngine.Create(TriggerType.Application, new Nep17NativeContractExtensions.ManualWitness(committeeAddress), snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings); + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "setMaxNotValidBeforeDelta", 100); + engine.LoadScript(script.ToArray()); + VMState vMState = engine.Execute(); + vMState.Should().Be(VMState.HALT); + NativeContract.Notary.GetMaxNotValidBeforeDelta(snapshot).Should().Be(100); + } + + [TestMethod] + public void Check_GetNotaryServiceFeePerKey() + { + NativeContract.Notary.GetNotaryServiceFeePerKey(_snapshot).Should().Be(10000000L); + } + + [TestMethod] + public void Check_SetNotaryServiceFeePerKey() + { + var snapshot = _snapshot.CreateSnapshot(); + // Fake blockchain + var persistingBlock = new Block { Header = new Header { Index = 1000 } }; + UInt160 committeeAddress = NativeContract.NEO.GetCommitteeAddress(snapshot); + + using var engine = ApplicationEngine.Create(TriggerType.Application, new Nep17NativeContractExtensions.ManualWitness(committeeAddress), snapshot, persistingBlock, settings: TestBlockchain.TheNeoSystem.Settings); + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.Notary.Hash, "setNotaryServiceFeePerKey", 100); + engine.LoadScript(script.ToArray()); + VMState vMState = engine.Execute(); + vMState.Should().Be(VMState.HALT); + NativeContract.Notary.GetNotaryServiceFeePerKey(snapshot).Should().Be(100); + } + + internal static StorageKey CreateStorageKey(byte prefix, uint key) + { + return CreateStorageKey(prefix, BitConverter.GetBytes(key)); + } + + internal static StorageKey CreateStorageKey(byte prefix, byte[] key = null) + { + StorageKey storageKey = new() + { + Id = NativeContract.NEO.Id, + Key = new byte[sizeof(byte) + (key?.Length ?? 0)] + }; + storageKey.Key[0] = prefix; + key?.CopyTo(storageKey.Key.AsSpan(1)); + return storageKey; + } + } +}