Skip to content
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

[SC] Embedded Smart Contracts #534

Open
wants to merge 35 commits into
base: infracontracts
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e67f72c
Add Multisig contract
quantumagi Apr 27, 2021
742ece4
Delegate to Authentication
quantumagi Apr 28, 2021
8329e6a
Add version
quantumagi Apr 28, 2021
d3196e3
Make VerifySignatures private
quantumagi Apr 28, 2021
341b4e1
Add assert
quantumagi Apr 28, 2021
f56193f
Add version
quantumagi Apr 28, 2021
df97431
Merge branch 'infracontracts' into multisig
quantumagi Apr 28, 2021
012caa2
Add tests
quantumagi Apr 29, 2021
a128ab0
Merge branch 'infracontracts' into multisig
quantumagi May 3, 2021
4269f0f
Update MultiSig contract and tests
quantumagi May 3, 2021
067c428
Enable on-chain and internal calling
quantumagi May 3, 2021
5acab15
Refactor
quantumagi May 5, 2021
4e94126
Refactor
quantumagi May 5, 2021
0258c4a
Refactor
quantumagi May 5, 2021
1f7e952
Refactor
quantumagi May 5, 2021
1e3119d
Refactor
quantumagi Jun 10, 2021
0200759
Refactor
quantumagi Jun 10, 2021
9552bc4
Refactor
quantumagi Jun 10, 2021
2e1b1b0
Add comment
quantumagi Jun 10, 2021
c30382b
Add EmbededContractType
quantumagi Jun 11, 2021
d41d3a4
Simplify EmbeddedContractContainer
quantumagi Jun 11, 2021
c990047
Rename
quantumagi Jun 11, 2021
82fcad9
Merge infracontracts
quantumagi Jun 11, 2021
dae9f00
Rename
quantumagi Jun 11, 2021
27b5d4b
Rename
quantumagi Jun 11, 2021
6b4e792
Resolve TODO
quantumagi Jun 11, 2021
47c915f
Fix comment
quantumagi Jun 11, 2021
a5369a3
Update test
quantumagi Jun 11, 2021
547a329
Remove unused class
quantumagi Jun 11, 2021
4d7d3da
Rename
quantumagi Jun 11, 2021
6e57908
Add singleton
quantumagi Jun 11, 2021
61196ed
Refactor EmbeddedContractAddress
quantumagi Jun 14, 2021
2323f22
Changes based on feedback
quantumagi Jun 14, 2021
c5ef05d
Changes based on feedback
quantumagi Jun 17, 2021
c056b46
Add comments to Multisig contract
quantumagi Jun 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/NBitcoin/Network.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ public interface IEmbeddedContractContainer
/// Returns the identifiers of all defined contracts, whether active or inactive.
/// </summary>
/// <returns></returns>
IEnumerable<uint160> GetContractIdentifiers();
IEnumerable<uint160> GetEmbeddedContractAddresses();

/// <summary>
/// Extracts the contract type and version from a contract hash.
Expand Down Expand Up @@ -241,6 +241,8 @@ public interface IFederations
IFederation GetFederationAtHeight(byte[] federationId, ulong blockHeight, uint256 blockHash);

IFederation GetOnlyFederation();

IEnumerable<IFederation> GetFederations();
}

public class Federations : IFederations
Expand All @@ -252,6 +254,11 @@ public Federations()
this.federations = new Dictionary<FederationId, IFederation>();
}

public IEnumerable<IFederation> GetFederations()
{
return this.federations.Values.AsEnumerable();
}

public IFederation GetFederation(FederationId federationId)
{
return this.federations.TryGetValue(federationId, out IFederation federation) ? federation : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,27 @@

namespace Stratis.Bitcoin.Features.SmartContracts.Tests.PoS
{
public class TestEmbeddedContract
{

}

public class EmbeddedContractContainerTests
{
[Fact]
public void CanUseEmbeddedContractContainer()
{
var network = new StraxMain();
EmbeddedContractIdentifier contractId = new EmbeddedContractIdentifier(1, 1);
uint160 embeddedContractAddress = EmbeddedContractAddress.Create(typeof(Authentication), 1);
var container = new EmbeddedContractContainer(
network,
new Dictionary<uint160, EmbeddedContractDescriptor> {
{ contractId, new EmbeddedContractDescriptor(typeof(TestEmbeddedContract).AssemblyQualifiedName,new[] { (1, (int?)10) }, "SystemContracts", true) } },
new List<EmbeddedContractVersion> {
{ new EmbeddedContractVersion(typeof(Authentication), 1, new[] { (1, (int?)10) }, "SystemContracts", true) } },
null);

uint160 id = container.GetContractIdentifiers().First();
uint160 address = container.GetEmbeddedContractAddresses().First();

Assert.True(EmbeddedContractIdentifier.IsEmbedded(contractId));
Assert.True(EmbeddedContractAddress.IsEmbedded(embeddedContractAddress));

Assert.True(container.TryGetContractTypeAndVersion(id, out string contractType, out uint version));
Assert.True(container.TryGetContractTypeAndVersion(address, out string contractType, out uint version));

Assert.Equal(typeof(TestEmbeddedContract).AssemblyQualifiedName, contractType);
Assert.Equal(contractId.Version, version);
Assert.Equal(typeof(Authentication).AssemblyQualifiedName, contractType);
Assert.Equal(embeddedContractAddress.GetEmbeddedVersion(), version);

ChainedHeader chainedHeader = new ChainedHeader(0, null, null) { };
var mockChainStore = new Mock<IChainStore>();
Expand All @@ -46,25 +41,25 @@ public void CanUseEmbeddedContractContainer()

// Active if previous header is at height 9.
chainedHeader.SetPrivatePropertyValue("Height", 9);
Assert.True(container.IsActive(id, chainedHeader, (h, d) => false));
Assert.True(container.IsActive(address, chainedHeader, (h, d) => false));

// Inactive if previous header is at height 10.
chainedHeader.SetPrivatePropertyValue("Height", 10);
Assert.False(container.IsActive(id, chainedHeader, (h, d) => false));
Assert.False(container.IsActive(address, chainedHeader, (h, d) => false));

// Inactive if previous header is at height 10 unless activated by BIP 9.
chainedHeader.SetPrivatePropertyValue("Height", 10);
Assert.True(container.IsActive(id, chainedHeader, (h, d) => true));
Assert.True(container.IsActive(address, chainedHeader, (h, d) => true));
}

[Fact]
public void CanDereferenceContractTypes()
{
foreach (Network network in new Network[] { new StraxMain(), new StraxTest(), new StraxRegTest() })
{
foreach (uint160 id in network.EmbeddedContractContainer.GetContractIdentifiers())
foreach (uint160 address in network.EmbeddedContractContainer.GetEmbeddedContractAddresses())
{
Assert.True(network.EmbeddedContractContainer.TryGetContractTypeAndVersion(id, out string typeName, out uint version));
Assert.True(network.EmbeddedContractContainer.TryGetContractTypeAndVersion(address, out string typeName, out uint version));

Assert.NotNull(Type.GetType(typeName));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
using Stratis.SmartContracts.CLR;
using ECRecover = Stratis.SCL.Crypto.ECRecover;

[EmbeddedContract(EmbeddedContractType.Authentication)]
public class Authentication : SmartContract
{
const string primaryGroup = "main";
private readonly uint version;

public Authentication(ISmartContractState state, Network network) : base(state)
{
uint version = new EmbeddedContractIdentifier(state.Message.ContractAddress.ToUint160()).Version;
uint version = state.Message.ContractAddress.ToUint160().GetEmbeddedVersion();

Assert(version == 1, "Only a version of 1 is supported.");

Expand Down
152 changes: 152 additions & 0 deletions src/Stratis.Bitcoin.Features.SmartContracts/Embedded/Multisig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System.Linq;
using NBitcoin;
using Stratis.SmartContracts;
using Stratis.SmartContracts.CLR;

[EmbeddedContract(EmbeddedContractType.Multisig)]
public class MultiSig : SmartContract
{
const string primaryGroup = "main";
private readonly uint version;
private readonly Authentication authentication;

public MultiSig(ISmartContractState state, IPersistenceStrategy persistenceStrategy, Network network) : base(state)
{
uint version = state.Message.ContractAddress.ToUint160().GetEmbeddedVersion();

Assert(version == 1, "Only a version of 1 is supported.");

this.version = version;
this.authentication = new Authentication(GetState(state, persistenceStrategy, EmbeddedContractAddress.Create(typeof(Authentication), 1)), network);
Copy link
Member

Choose a reason for hiding this comment

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

It seems it makes more sense to me that Authentication deployed as another smart contract and MultiSig has to make external calls to that contract.

Copy link
Contributor Author

@quantumagi quantumagi Jun 17, 2021

Choose a reason for hiding this comment

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

Authentication is being deployed as a separate contract. In this case the contract can be called directly, which seems faster, so why not? :)

Copy link
Member

Choose a reason for hiding this comment

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

This decision bring Initialized logic and it is not what expected in regular design for smart contracts.

Copy link
Contributor Author

@quantumagi quantumagi Jun 17, 2021

Choose a reason for hiding this comment

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

The "Initialized" flag is required due to the absence of a deployment step for embedded smart contracts.

#534 (comment)


// Exit if already initialized.
if (this.Initialized)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this happen?

I assume constructor is called only when contract is deployed, so how can it be initialized?

Copy link
Contributor Author

@quantumagi quantumagi Jun 14, 2021

Choose a reason for hiding this comment

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

The embedded contracts act a bit like transient singletons. When called via the EmbeddedContractMachine VM this constructor, rather than the default constructor (via reflection), is used to instantiate the contract to be called. Contract objects are not kept in memory as that may open the door to bad programming practices such as passing in-memory variables between calls. Initialized keeps track of once-off state initialization to be performed by the contract.

Copy link
Member

Choose a reason for hiding this comment

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

@rowandh Wdyt about this ?

Copy link
Contributor Author

@quantumagi quantumagi Nov 2, 2021

Choose a reason for hiding this comment

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

I.e. for embedded contracts there is no enforced deployment step so the state must be initialized the first time it is used. The Initialized value itself is set and stored in the state to indicate that the state is initialized. Keep in mind that this constructor is called repeatedly as a result of a new contract object being used for each call. Such a new object may detect that the state is already initialized and skip the initialization.

return;

foreach (IFederation federation in network.Federations.GetFederations())
{
(PubKey[] pubKeys, int signaturesRequired) = federation.GetFederationDetails();
AddFederation(federation.Id.ToHex(network), (uint)signaturesRequired, pubKeys.Select(pk => pk.ToHex()).ToArray());
}

this.Initialized = true;
}

private ISmartContractState GetState(ISmartContractState state, IPersistenceStrategy persistenceStrategy, uint160 address)
{
return new SmartContractState(state.Block, state.Message, new PersistentState(persistenceStrategy, state.Serializer, address),
state.Serializer, state.ContractLogger, state.InternalTransactionExecutor, state.InternalHashHelper, state.GetBalance);
}

public bool Initialized
{
get => this.State.GetBool("Initialized");
set => this.State.SetBool("Initialized", value);
}

private void VerifySignatures(string group, byte[] signatures, string authorizationChallenge)
{
this.authentication.VerifySignatures(group, signatures, authorizationChallenge);
}

private void AddFederation(string federationId, uint quorum, string[] pubKeys)
{
SetFederationMembers(federationId, pubKeys);
SetFederationQuorum(federationId, quorum);
}

public string[] GetFederationMembers(string federationId)
{
Assert(!string.IsNullOrEmpty(federationId));
return this.State.GetArray<string>($"Members:{federationId}");
}

private void SetFederationMembers(string federationId, string[] values)
{
this.State.SetArray($"Members:{federationId}", values);
}

public uint GetFederationQuorum(string federationId)
{
Assert(!string.IsNullOrEmpty(federationId));
return this.State.GetUInt32($"Quorum:{federationId}");
}

private void SetFederationQuorum(string federationId, uint quorum)
{
this.State.SetUInt32($"Quorum:{federationId}", quorum);
}

private uint GetFederationNonce(string federationId)
{
return this.State.GetUInt32($"Nonce:{federationId}");
}

private void SetFederationNonce(string federationId, uint value)
{
this.State.SetUInt32($"Nonce:{federationId}", value);
}

public void AddMember(byte[] signatures, string federationId, string pubKey, uint newSize, uint newQuorum)
{
Assert(!string.IsNullOrEmpty(federationId));
Assert(newSize >= newQuorum, "The number of signatories can't be less than the quorum.");
Assert(new PubKey(pubKey).ToHex().ToUpper() == pubKey.ToUpper());

string[] signatories = this.GetFederationMembers(federationId);
foreach (string signatory in signatories)
Assert(signatory != pubKey, "The signatory already exists.");

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

// The nonce is used to prevent replay attacks.
uint nonce = this.GetFederationNonce(federationId);

// 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(primaryGroup, signatures, $"{nameof(AddMember)}(Nonce:{nonce},FederationId:{federationId},PubKey:{pubKey},NewSize:{newSize},NewQuorum:{newQuorum})");

System.Array.Resize(ref signatories, signatories.Length + 1);
signatories[signatories.Length - 1] = pubKey;

this.SetFederationMembers(federationId, signatories);
this.SetFederationQuorum(federationId, newQuorum);
this.SetFederationNonce(federationId, nonce + 1);
}

public void RemoveMember(byte[] signatures, string federationId, string pubKey, uint newSize, uint newQuorum)
{
Assert(!string.IsNullOrEmpty(federationId));
Assert(newSize >= newQuorum, "The number of signatories can't be less than the quorum.");
Assert(new PubKey(pubKey).ToHex().ToUpper() == pubKey.ToUpper());

string[] prevSignatories = this.GetFederationMembers(federationId);
string[] signatories = new string[prevSignatories.Length - 1];

int i = 0;
foreach (string item in prevSignatories)
{
if (item == pubKey)
{
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.GetFederationNonce(federationId);

// 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(primaryGroup, signatures, $"{nameof(RemoveMember)}(Nonce:{nonce},FederationId:{federationId},PubKey:{pubKey},NewSize:{newSize},NewQuorum:{newQuorum})");

this.SetFederationMembers(federationId, signatories);
this.SetFederationQuorum(federationId, newQuorum);
this.SetFederationNonce(federationId, nonce + 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@ namespace Stratis.Bitcoin.Features.SmartContracts.PoS
/// <summary>
/// Holds the information and logic to determines whether an embedded system contract should be active.
/// </summary>
public class EmbeddedContractDescriptor
public class EmbeddedContractVersion
{
public EmbeddedContractDescriptor(string contractType, (int start, int? end)[] activationHistory, string activationName, bool activationState)
public EmbeddedContractVersion(Type contractType, uint version, (int start, int? end)[] activationHistory, string activationName, bool activationState)
{
this.ContractType = contractType;
this.Version = version;
this.ActivationHistory = activationHistory;
this.ActivationName = activationName;
this.ActivationState = activationState;
}

/// <summary>The contract version that this information applies to.</summary>
public uint Version { get; private set; }

/// <summary>The <see cref="Type.AssemblyQualifiedName"/> of the contract.</summary>
public string ContractType { get; private set; }
public Type ContractType { get; private set; }

/// <summary>The address of the contract.</summary>
public uint160 Address => EmbeddedContractAddress.Create(this.ContractType, this.Version);

/// <summary>History of block ranges over which contracts were active.
/// The BIP9 Deployments array is sometimes cleaned up and the information therein has to be transferred here.</summary>
Expand All @@ -42,7 +49,7 @@ public class EmbeddedContractContainer : IEmbeddedContractContainer
private readonly Network network;

/// <summary>The embedded contracts for this network.</summary>
private Dictionary<uint160, EmbeddedContractDescriptor> contracts;
private Dictionary<uint160, EmbeddedContractVersion> contracts;

/// <summary>
/// The addresses (defaults) and quorum of the primary authenticators of this network.
Expand All @@ -52,36 +59,38 @@ public class EmbeddedContractContainer : IEmbeddedContractContainer
/// <summary>The class constructor.</summary>
public EmbeddedContractContainer(
Network network,
Dictionary<uint160, EmbeddedContractDescriptor> contracts,
List<EmbeddedContractVersion> contracts,
PrimaryAuthenticators primaryAuthenticators)
{
this.network = network;
this.contracts = contracts;
this.contracts = new Dictionary<uint160, EmbeddedContractVersion>();
foreach (EmbeddedContractVersion contract in contracts)
this.contracts.Add(contract.Address, contract);
this.PrimaryAuthenticators = primaryAuthenticators;
}

/// <inheritdoc/>
public bool TryGetContractTypeAndVersion(uint160 id, out string contractType, out uint version)
public bool TryGetContractTypeAndVersion(uint160 address, out string contractType, out uint version)
{
Guard.Assert(EmbeddedContractIdentifier.IsEmbedded(id));
Guard.Assert(EmbeddedContractAddress.IsEmbedded(address));

version = new EmbeddedContractIdentifier(id).Version;
version = address.GetEmbeddedVersion();

if (!this.contracts.TryGetValue(id, out EmbeddedContractDescriptor contract))
if (!this.contracts.TryGetValue(address, out EmbeddedContractVersion contract))
{
contractType = null;
return false;
}

contractType = contract.ContractType;
contractType = contract.ContractType.AssemblyQualifiedName;

return true;
}

/// <inheritdoc/>
public bool IsActive(uint160 id, ChainedHeader previousHeader, Func<ChainedHeader, int, bool> deploymentCondition)
public bool IsActive(uint160 address, ChainedHeader previousHeader, Func<ChainedHeader, int, bool> deploymentCondition)
{
if (!this.contracts.TryGetValue(id, out EmbeddedContractDescriptor contract))
if (!this.contracts.TryGetValue(address, out EmbeddedContractVersion contract))
return false;

bool isActive = contract.ActivationHistory.Any(r => (previousHeader.Height + 1) >= r.start && (r.end == null || (previousHeader.Height + 1) <= r.end));
Expand All @@ -97,7 +106,7 @@ public bool IsActive(uint160 id, ChainedHeader previousHeader, Func<ChainedHeade
}

/// <inheritdoc/>
public IEnumerable<uint160> GetContractIdentifiers()
public IEnumerable<uint160> GetEmbeddedContractAddresses()
{
return this.contracts.Select(c => c.Key);
}
Expand Down
Loading