Skip to content

Conversation

@quantumagi
Copy link
Contributor

@quantumagi quantumagi commented Mar 30, 2021

Note that this approach, relying on signatures, does not require additional external permission management.

Example Authorization:

- Anyone calls the "WhiteList" method on the "Dictionary" contract. The method responds with an authorization challenge:
  - E.g. "WhiteList(Nonce:0,CodeHash:123,LastAddress:,Name:)"
- Each signatory calls the legacy "SignMessage" API to obtain a signature for the challenge.
- The submitter calls the "WhiteList" method again with all the signatures.
- The contract is now successfully white-listed and can now be deployed.
- Following contract deployment a contract address will be available. 
- This process is repeated to provide the contract address and a friendly name (e.g. "Multisig").
- Now all nodes that rely on the information from the "Multisig" contract will be able to locate the new contract.

Note: The contract hash of the "System Contract Dictionary" itself will be hard-coded (as being white-listed) in the code base. Initially only the first deployment will be used to determine the contract address unless it is self-listed with a different address in which case we will "follow the chain" to find the most recent version.

Merge PR #509 first.

@noescape00
Copy link
Contributor

I prefer this one over 489
Please let me know when it's ready for full review

@quantumagi quantumagi marked this pull request as ready for review April 8, 2021 05:45
@quantumagi quantumagi requested review from noescape00 and rowandh April 8, 2021 07:51
@quantumagi
Copy link
Contributor Author

@noescape00 , I've added some more functionality if you would like to have a look now.

for (int i = 0; i < signatories.Length; i++)
Assert(signatories[i] != address, "The signatory already exists.");

Assert((signatories.Length + 1) == newSize, "The expected size is incorrect.");

This comment was marked as resolved.

Copy link
Contributor Author

@quantumagi quantumagi Apr 10, 2021

Choose a reason for hiding this comment

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

Equality check should not be negated in here?

It is correct afaik. The error is displayed when the assertion fails.

@quantumagi quantumagi changed the title [Infra] System Contract Dictionary (Candidate 2) [Infra] System Contract Dictionary Apr 15, 2021
Copy link
Contributor

@YakupIpek YakupIpek left a comment

Choose a reason for hiding this comment

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

I added my feedbacks and i did not leave same feedbacks on duplicated issues. So i will review again after you submited changes.

I suggest you to review and follow style of DAO contract
https://github.com/stratisproject/CirrusSmartContracts/blob/master/Testnet/DAOContract/DAOContract/DAOContract.cs

@@ -0,0 +1,205 @@
using Stratis.SmartContracts;
using ECRecover=Stratis.SCL.Crypto.ECRecover;
Copy link
Contributor

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 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a reason?

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems fine.


public SystemContractsDictionary(ISmartContractState state) : base(state)
{
this.State.SetArray($"Signatories:{primaryGroup}", new[] { new Address(0, 0, 0, 0, 0), new Address(0, 0, 0, 0, 1), new Address(0, 0, 0, 0, 2) });
Copy link
Contributor

Choose a reason for hiding this comment

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

These value should be hard coded ?

Copy link
Contributor Author

@quantumagi quantumagi Apr 15, 2021

Choose a reason for hiding this comment

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

These "default" values should be updated at some stage with the correct default values. The AddSignatory and RemoveSignatory will allow the defaults to be changed. I am worried about passing the defaults to the constructor as maybe this will lead to reduced or single person security and maybe make a mockery of the multiple person security this wants to offer.

Copy link
Contributor

Choose a reason for hiding this comment

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

State should be access by getters/setters in everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

private void VerifySignatures(byte[] signatures, string authorizationChallenge)
{
string[] sigs = this.Serializer.ToArray<string>(signatures);
Assert(ECRecover.TryVerifySignatures(sigs, System.Text.Encoding.ASCII.GetBytes(authorizationChallenge), this.Signatories, out Address[] verifiedSignatures) && verifiedSignatures.Length >= this.Quorum,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you move namespace to top of page ?

Copy link
Contributor

@YakupIpek YakupIpek Apr 15, 2021

Choose a reason for hiding this comment

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

IMO ECRecover.TryVerifySignatures should be GetVerifiedSignatories and return addresses otherwise Try prefix and out param is quite confusing to me. I suggest code like below.

        var challange = Encoding.ASCII.GetBytes(authorizationChallenge);

        var verifieds = ECRecover.GEtVerifiedSignatures(sigs, challange, this.Signatories);

        Assert(verifieds.Length >= this.Quorum, $"Please provide {this.Quorum} valid signatures for '{authorizationChallenge}'.");

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rowandh , this was your suggestion, wdyt? I'm on the fence.

Copy link
Contributor

Choose a reason for hiding this comment

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

When TryVerifySignatures will return false ?
When there is some verified signatories ? No
When there is no any verified signatories ? Still no
These are very unexpected behavior from this method.

It only returns false if provided parameters are null. It is clear that expected behavior of the method is return verified signatories.

Copy link
Contributor Author

@quantumagi quantumagi Apr 16, 2021

Choose a reason for hiding this comment

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

That is how it was and frankly I had similar thoughts as Yakup. @rowandh should I change this back?


public Address[] GetSignatories(string group)
{
Assert(!string.IsNullOrEmpty(group));
Copy link
Contributor

Choose a reason for hiding this comment

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

This assert is not necessary so you can remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.GetUInt32($"Quorum:{group}");
}

public void AddSignatory(byte[] signatures, string group, Address address, uint newSize, uint newQuorum)
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.


Address[] signatories = this.GetSignatories(group);
for (int i = 0; i < signatories.Length; i++)
Assert(signatories[i] != address, "The signatory already exists.");
Copy link
Contributor

@YakupIpek YakupIpek Apr 15, 2021

Choose a reason for hiding this comment

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

Adding, removing or checking existence of signatories cause iterative codes and resizing array costs. Alternatively we can index signatories in state like

"Signatories:{signature}" => group

After that you can quickly verify it is existence in dictionary

Another key pair alternative would be

"Signatories:{group}:{signature}" => bool(true)

@rowandh wdty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would shift the problem of being able to obtain a list of all signatories.

Copy link
Contributor

@YakupIpek YakupIpek Apr 15, 2021

Choose a reason for hiding this comment

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

Proably "Signatories:{signature}" => group wouldn't work because same address can be added to different groups so second solution is only solution.

Copy link
Contributor Author

@quantumagi quantumagi Apr 16, 2021

Choose a reason for hiding this comment

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

@YakupIpek , given your approach how would you list all the signatories of a group? I've optimized for this method since the other methods would occur far less frequently:

    public Address[] GetSignatories(string group)
    {
        return this.State.GetArray<Address>($"Signatories:{group}");
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not quite sure which path we have to follow right now. You can still separate index like $"Signatories:{group}:{index}" but you have to iterate it at the end if you want to obtain all of them.
Lets wait @rowandh feedback.

}

public void RemoveSignatory(byte[] signatures, string group, Address address, uint newSize, uint newQuorum)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

These methods can be called by anyone ?

Copy link
Contributor Author

@quantumagi quantumagi Apr 15, 2021

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 ?

Copy link
Contributor Author

@quantumagi quantumagi Apr 16, 2021

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I understand what you're saying. Will mull it over.


public UInt256 GetCodeHash(string name)
{
Assert(!string.IsNullOrEmpty(name));
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

@YakupIpek YakupIpek Apr 16, 2021

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is common way we did in other contracts ussually.

Copy link
Contributor Author

@quantumagi quantumagi Apr 16, 2021

Choose a reason for hiding this comment

The 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.

so i have to verify exception + default value check.

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.

string authorizationChallenge;
if (whiteListEntry.CodeHash == default(UInt256))
{
authorizationChallenge = $"{nameof(WhiteList)}(Nonce:{nonce},CodeHash:{codeHash},LastAddress:{lastAddress},Name:{name})";
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not as below ?

authorizationChallenge = $"{nameof(WhiteList)}:{nonce}:{codeHash},{lastAddress},{name})"

Copy link
Contributor Author

@quantumagi quantumagi Apr 15, 2021

Choose a reason for hiding this comment

The 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.

private void VerifySignatures(byte[] signatures, string authorizationChallenge)
{
string[] sigs = this.Serializer.ToArray<string>(signatures);
Assert(ECRecover.TryVerifySignatures(sigs, System.Text.Encoding.ASCII.GetBytes(authorizationChallenge), this.Signatories, out Address[] verifiedSignatures) && verifiedSignatures.Length >= this.Quorum,
Copy link
Contributor

Choose a reason for hiding this comment

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

When TryVerifySignatures will return false ?
When there is some verified signatories ? No
When there is no any verified signatories ? Still no
These are very unexpected behavior from this method.

It only returns false if provided parameters are null. It is clear that expected behavior of the method is return verified signatories.


Address[] signatories = this.GetSignatories(group);
for (int i = 0; i < signatories.Length; i++)
Assert(signatories[i] != address, "The signatory already exists.");
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not quite sure which path we have to follow right now. You can still separate index like $"Signatories:{group}:{index}" but you have to iterate it at the end if you want to obtain all of them.
Lets wait @rowandh feedback.


public UInt256 GetCodeHash(string name)
{
Assert(!string.IsNullOrEmpty(name));
Copy link
Contributor

@YakupIpek YakupIpek Apr 16, 2021

Choose a reason for hiding this comment

The 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.


public UInt256 GetCodeHash(string name)
{
Assert(!string.IsNullOrEmpty(name));
Copy link
Contributor

Choose a reason for hiding this comment

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

This is common way we did in other contracts ussually.

@quantumagi
Copy link
Contributor Author

#490 (comment)

@noescape00
Copy link
Contributor

noescape00 commented Apr 16, 2021

Please add xmldoc for all methods of SystemContractsDictionary that are not pretty much self explanatory, plus general description of the class

public static bool TryVerifySignatures(string[] signatures, byte[] message, Address[] addresses, out Address[] verifiedAddresses)
/// <param name="addresses">The addresses returned are intersected with these addresses (if not <c>null</c>).</param>
/// <returns>The list of addresses that produced the signatures and constrained to the list provided in <paramref name="addresses"/>.</returns>
public static Address[] GetVerifiedSignatures(string[] signatures, string message, Address[] addresses)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an API change to the SCL library. I don't think these should be made randomly in PRs like this but rather as part of a structured API design for the initial SCL release.

@quantumagi
Copy link
Contributor Author

Moved to other PRs.

@quantumagi quantumagi closed this Apr 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants