-
Notifications
You must be signed in to change notification settings - Fork 72
[Infra] System Contract Dictionary #490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2ffef77
b286f79
5abe3b8
8d02279
c0264b1
bf6e443
5018f77
b2f092d
d5f27cd
d7e5d4d
61cddba
d550216
d12435d
77e0ad2
b7036e4
95df108
874f26b
5bb2101
62641be
a7f457c
aa9794d
bd66981
7b76017
dcded24
bec8fff
5695579
d2f02ad
d08549c
02357c2
523d4bd
6b8e48d
6e5e1f7
5333622
8d0d17e
ce850ee
a535af1
3ad0954
b659b86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,288 @@ | ||
| using Stratis.SmartContracts; | ||
| using ECRecover=Stratis.SCL.Crypto.ECRecover; | ||
|
|
||
| public struct WhiteListEntry | ||
| { | ||
| public UInt256 CodeHash; | ||
| public Address LastAddress; | ||
| public string Name; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The primary purpose is to provide a mechanism by which smart contracts are whitelisted | ||
| /// and secondly to maintain the list of authenticators (signatories) required to do so. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// All method calls that change the whitelist or signatories require multiple signatories | ||
| /// as specified by the current list of defined signatories and the quorum requirement. | ||
| /// </remarks> | ||
| public class SystemContractsDictionary : SmartContract | ||
| { | ||
| const string primaryGroup = "main"; | ||
|
|
||
| /// <summary> | ||
| /// Initializes the contract with the default (or initial) set of signatories which are required when calling some of the methods. | ||
| /// </summary> | ||
| /// <param name="state">Contract state.</param> | ||
| /// <param name="signatories">Initial set of signatories.</param> | ||
| /// <param name="quorum">Initial quorum requirement/</param> | ||
| public SystemContractsDictionary(ISmartContractState state, Address[] signatories, uint quorum) : base(state) | ||
| { | ||
| this.SetSignatories(primaryGroup, signatories); | ||
| this.SetQuorum(primaryGroup, quorum); | ||
| } | ||
|
|
||
| private Address[] Signatories => GetSignatories(primaryGroup); | ||
|
|
||
| private uint Quorum => GetQuorum(primaryGroup); | ||
|
|
||
| private void VerifySignatures(byte[] signatures, string authorizationChallenge) | ||
| { | ||
| string[] sigs = this.Serializer.ToArray<string>(signatures); | ||
|
|
||
| Assert(ECRecover.TryGetVerifiedSignatures(sigs, authorizationChallenge, this.Signatories, out var verifieds), "Invalid signatures"); | ||
|
|
||
| Assert(verifieds.Length >= this.Quorum, $"Please provide {this.Quorum} valid signatures for '{authorizationChallenge}'."); | ||
| } | ||
|
|
||
| public Address[] GetSignatories(string group) | ||
| { | ||
| Assert(!string.IsNullOrEmpty(group)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assert is not necessary so you can remove it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does make a difference by throwing an error versus simply returning the default. Intuitively throwing an error makes more sense but perhaps there is some reason you don't want to do that for smart contracts. @rowandh , wdyt?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might save a small amount of gas by doing this. Seems fine. |
||
| return this.State.GetArray<Address>($"Signatories:{group}"); | ||
| } | ||
|
|
||
| private void SetSignatories(string group, Address[] values) | ||
| { | ||
| this.State.SetArray($"Signatories:{group}", values); | ||
| } | ||
|
|
||
| public uint GetQuorum(string group) | ||
| { | ||
| Assert(!string.IsNullOrEmpty(group)); | ||
| return this.State.GetUInt32($"Quorum:{group}"); | ||
| } | ||
|
|
||
| private void SetQuorum(string group, uint value) | ||
| { | ||
| this.State.SetUInt32($"Quorum:{group}", value); | ||
| } | ||
|
|
||
| private uint GetGroupNonce(string group) | ||
| { | ||
| return this.State.GetUInt32($"GroupNonce:{group}"); | ||
| } | ||
|
|
||
| private void SetGroupNonce(string group, uint value) | ||
| { | ||
| this.State.SetUInt32($"GroupNonce:{group}", value); | ||
| } | ||
|
|
||
| public void AddSignatory(byte[] signatures, string group, Address address, uint newSize, uint newQuorum) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Who is going to call this method ? Owner of the contract or anyone ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyone that's willing to pay the gas cost to execute it. However signatures are checked before any updates are made. |
||
| { | ||
| Assert(!string.IsNullOrEmpty(group)); | ||
| Assert(newSize >= newQuorum, "The number of signatories can't be less than the quorum."); | ||
|
|
||
| Address[] signatories = this.GetSignatories(group); | ||
| foreach (Address signatory in signatories) | ||
| Assert(signatory != address, "The signatory already exists."); | ||
|
|
||
| Assert((signatories.Length + 1) == newSize, "The expected size is incorrect."); | ||
This comment was marked as resolved.
Sorry, something went wrong.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is correct afaik. The error is displayed when the assertion fails. |
||
|
|
||
| // The nonce is used to prevent replay attacks. | ||
| uint nonce = this.GetGroupNonce(group); | ||
|
|
||
| // Validate or provide a unique challenge to the signatories that depends on the exact action being performed. | ||
| // If the signatures are missing or fail validation contract execution will stop here. | ||
| this.VerifySignatures(signatures, $"{nameof(AddSignatory)}(Nonce:{nonce},Group:{group},Address:{address},NewSize:{newSize},NewQuorum:{newQuorum})"); | ||
|
|
||
| System.Array.Resize(ref signatories, signatories.Length + 1); | ||
| signatories[signatories.Length - 1] = address; | ||
|
|
||
| this.SetSignatories(group, signatories); | ||
| this.SetQuorum(group, newQuorum); | ||
| this.SetGroupNonce(group, nonce + 1); | ||
| } | ||
|
|
||
| public void RemoveSignatory(byte[] signatures, string group, Address address, uint newSize, uint newQuorum) | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These methods can be called by anyone ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyone that's willing to pay the gas cost to execute it. However signatures are checked before any updates are made.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is really needed to specify newSize and newQuorum, why not to automate it ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really want the user to be very exact about the change being accomplished especially due to how important the quorum count is in relation to the signatory count. The user must know exactly what is being accomplished in that regards and show that knowledge by specifying the values explicitly. All the signatories will also see that information in the security challenge when signing.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hımm but the transaction is going to fail and cost fee if anyone going to call these methods in same block or in prev blocks.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I understand what you're saying. Will mull it over. |
||
| Assert(!string.IsNullOrEmpty(group)); | ||
| Assert(newSize >= newQuorum, "The number of signatories can't be less than the quorum."); | ||
|
|
||
| Address[] prevSignatories = this.GetSignatories(group); | ||
| Address[] signatories = new Address[prevSignatories.Length - 1]; | ||
|
|
||
| int i = 0; | ||
| foreach (Address item in prevSignatories) | ||
| { | ||
| if (item == address) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| Assert(signatories.Length != i, "The signatory does not exist."); | ||
|
|
||
| signatories[i++] = item; | ||
| } | ||
|
|
||
| Assert(newSize == signatories.Length, "The expected size is incorrect."); | ||
|
|
||
| // The nonce is used to prevent replay attacks. | ||
| uint nonce = this.GetGroupNonce(group); | ||
|
|
||
| // Validate or provide a unique challenge to the signatories that depends on the exact action being performed. | ||
| // If the signatures are missing or fail validation contract execution will stop here. | ||
| this.VerifySignatures(signatures, $"{nameof(RemoveSignatory)}(Nonce:{nonce},Group:{group},Address:{address},NewSize:{newSize},NewQuorum:{newQuorum})"); | ||
|
|
||
| this.SetSignatories(group, signatories); | ||
| this.SetQuorum(group, newQuorum); | ||
| this.SetGroupNonce(group, nonce + 1); | ||
| } | ||
|
|
||
| public bool IsWhiteListed(UInt256 codeHash) | ||
| { | ||
| Assert(codeHash != default(UInt256)); | ||
|
|
||
| WhiteListEntry whiteListEntry = this.GetWhiteListEntry(codeHash); | ||
|
|
||
| return whiteListEntry.CodeHash != default(UInt256); | ||
| } | ||
|
|
||
| public UInt256 GetCodeHash(string name) | ||
| { | ||
| Assert(!string.IsNullOrEmpty(name)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imo these asserts are not necessary. If someone send null,empty or none existence key then we return default value already from state.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does make a difference by throwing an error versus simply returning the default. Intuitively throwing an error makes more sense but perhaps there is some reason you don't want to do that for smart contracts. @rowandh , wdyt?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As caller of the method, i should to verify that there is hash code or not for given name parameter in any case and i do it by checking its value is default or not. But you added assertion, in the case of it is null so i have to verify exception + default value check. We should not optimize code for exceptional cases.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is common way we did in other contracts ussually.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is insufficient checks something like "Quorum:" could obtain a value, then when "Quorum:" is accessed later a junk value could be returned.
You don't have to verify exception if you're not passing null values. If you are unexpectedly passing null values then it better to get an error than some junk value you may proceed processing with. |
||
|
|
||
| return this.State.GetUInt256($"ByName:{name}"); | ||
| } | ||
|
|
||
| private void SetCodeHash(string name, UInt256 codeHash) | ||
| { | ||
| this.State.SetUInt256($"ByName:{name}", codeHash); | ||
| } | ||
|
|
||
| private void ClearCodeHash(string name) | ||
| { | ||
| this.State.Clear($"ByName:{name}"); | ||
| } | ||
|
|
||
| public Address GetContractAddress(string name) | ||
| { | ||
| UInt256 codeHash = GetCodeHash(name); | ||
|
|
||
| if (codeHash == default(UInt256)) | ||
| return default(Address); | ||
|
|
||
| WhiteListEntry whiteListEntry = this.GetWhiteListEntry(codeHash); | ||
|
|
||
| return whiteListEntry.LastAddress; | ||
| } | ||
|
|
||
| private WhiteListEntry GetWhiteListEntry(UInt256 codeHash) | ||
| { | ||
| return this.State.GetStruct<WhiteListEntry>($"CodeHash:{codeHash}"); | ||
| } | ||
|
|
||
| private void SetWhiteListEntry(UInt256 codeHash, WhiteListEntry whiteListEntry) | ||
| { | ||
| this.State.SetStruct<WhiteListEntry>($"CodeHash:{codeHash}", whiteListEntry); | ||
| } | ||
|
|
||
| private void ClearWhiteListEntry(UInt256 codeHash) | ||
| { | ||
| this.State.Clear($"CodeHash:{codeHash}"); | ||
| } | ||
|
|
||
| public Address GetContractAddress(UInt256 codeHash) | ||
| { | ||
| Assert(codeHash != default(UInt256)); | ||
|
|
||
| return this.GetWhiteListEntry(codeHash).LastAddress; | ||
| } | ||
|
|
||
| private uint GetCodeNonce(UInt256 codeHash) | ||
| { | ||
| return this.State.GetUInt32($"Nonce:{codeHash}"); | ||
| } | ||
|
|
||
| private void SetCodeNonce(UInt256 codeHash, uint nonce) | ||
| { | ||
| this.State.SetUInt32($"Nonce:{codeHash}", nonce); | ||
| } | ||
|
|
||
| public void WhiteList(byte[] signatures, UInt256 codeHash, Address lastAddress, string name) | ||
| { | ||
| Assert(signatures != null); | ||
| Assert(codeHash != default(UInt256)); | ||
|
|
||
| UInt256 codeHashKey = codeHash; | ||
| WhiteListEntry whiteListEntry; | ||
|
|
||
| // If the name already exists then use it to locate the whitelist entry to update. | ||
| if (!string.IsNullOrEmpty(name)) | ||
| { | ||
| codeHashKey = this.GetCodeHash(name); | ||
|
|
||
| if (codeHashKey == default(UInt256)) | ||
| codeHashKey = codeHash; | ||
| } | ||
|
|
||
| whiteListEntry = this.GetWhiteListEntry(codeHashKey); | ||
|
|
||
| // The nonce is used to prevent replay attacks. | ||
| uint nonce = this.GetCodeNonce(codeHash); | ||
|
|
||
| // Validate or provide a unique challenge to the signatories that depends on the exact action being performed. | ||
| string authorizationChallenge; | ||
| if (whiteListEntry.CodeHash == default(UInt256)) | ||
| { | ||
| authorizationChallenge = $"{nameof(WhiteList)}(Nonce:{nonce},CodeHash:{codeHash},LastAddress:{lastAddress},Name:{name})"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not as below ? authorizationChallenge = $"{nameof(WhiteList)}:{nonce}:{codeHash},{lastAddress},{name})"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's meant to be as human readable as possible to ensure signatories can read it and know what they are signing. |
||
| } | ||
| else | ||
| { | ||
| Assert(whiteListEntry.CodeHash != codeHash || whiteListEntry.LastAddress != lastAddress || whiteListEntry.Name != name, "Nothing changed."); | ||
| authorizationChallenge = $"{nameof(WhiteList)}(Nonce:{nonce},CodeHash:{whiteListEntry.CodeHash}=>{codeHash},LastAddress:{whiteListEntry.LastAddress}=>{lastAddress},Name:{whiteListEntry.Name}=>{name})"; | ||
| } | ||
|
|
||
| // If the signatures are missing or fail validation contract execution will stop here. | ||
| this.VerifySignatures(signatures, authorizationChallenge); | ||
|
|
||
| if (whiteListEntry.CodeHash != default(UInt256)) | ||
| { | ||
| if (codeHash != whiteListEntry.CodeHash) | ||
| this.ClearWhiteListEntry(whiteListEntry.CodeHash); | ||
|
|
||
| if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(whiteListEntry.Name)) | ||
| this.ClearCodeHash(whiteListEntry.Name); | ||
| } | ||
|
|
||
| whiteListEntry.CodeHash = codeHash; | ||
| whiteListEntry.LastAddress = lastAddress; | ||
| whiteListEntry.Name = name; | ||
|
|
||
| this.SetWhiteListEntry(codeHash, whiteListEntry); | ||
| this.SetCodeHash(name, codeHash); | ||
| this.SetCodeNonce(codeHash, nonce + 1); | ||
| } | ||
|
|
||
| public void BlackList(byte[] signatures, UInt256 codeHash) | ||
| { | ||
| Assert(signatures != null); | ||
| Assert(codeHash != default(UInt256)); | ||
|
|
||
| WhiteListEntry whiteListEntry = this.GetWhiteListEntry(codeHash); | ||
|
|
||
| Assert(whiteListEntry.CodeHash != default(UInt256), "The entry does not exist."); | ||
|
|
||
| // The nonce is used to prevent replay attacks. | ||
| uint nonce = this.GetCodeNonce(codeHash); | ||
|
|
||
| // Validate or provide a unique challenge to the signatories that depends on the exact action being performed. | ||
| // If the signatures are missing or fail validation contract execution will stop here. | ||
| this.VerifySignatures(signatures, $"{nameof(BlackList)}(Nonce:{nonce},CodeHash:{whiteListEntry.CodeHash},LastAddress:{whiteListEntry.LastAddress},Name:{whiteListEntry.Name})"); | ||
|
|
||
| this.ClearWhiteListEntry(codeHash); | ||
| this.ClearCodeHash(whiteListEntry.Name); | ||
|
|
||
| // Don't remove the nonce. Just increment it. | ||
| this.SetCodeNonce(codeHash, nonce + 1); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you import just namespace in here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These type of using statements for fixing conflicts when one type is exist in more than 2 namespace so this is not a case in here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... It would protect against any conflicts that may arise in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems fine.