Skip to content

Commit

Permalink
Merge pull request stratisproject#312 from rowandh/proposer-build-tra…
Browse files Browse the repository at this point in the history
…nsaction

Build signed transaction in proposer
  • Loading branch information
codingupastorm authored Apr 22, 2020
2 parents cc208dd + f402c41 commit 2d770fb
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Moq;
using NBitcoin;
using Newtonsoft.Json;
using Stratis.Feature.PoA.Tokenless.Consensus;
using Stratis.Feature.PoA.Tokenless.Endorsement;
using Stratis.SmartContracts.CLR;
using Stratis.SmartContracts.Core;
using Stratis.SmartContracts.Core.ReadWrite;
using Stratis.SmartContracts.Core.Util;
using Xunit;

namespace Stratis.Feature.PoA.Tokenless.Tests
{
public class EndorsedTransactionBuilderTests
{
private readonly List<SignedProposalResponse> proposalResponses;
private readonly Key key;
private readonly ProposalResponse response;
private readonly TokenlessNetwork network;
private readonly TokenlessSigner signer;
private readonly Key transactionSigningKey;

public EndorsedTransactionBuilderTests()
{
this.network = new TokenlessNetwork();
this.signer = new TokenlessSigner(this.network, new SenderRetriever());

this.key = new Key();
this.transactionSigningKey = new Key();

this.response = new ProposalResponse
{
ReadWriteSet = new ReadWriteSet
{
Reads = new List<ReadItem>
{
new ReadItem {ContractAddress = uint160.One, Key = new byte[] {0xAA}, Version = "1"}
},
Writes = new List<WriteItem>
{
new WriteItem
{
ContractAddress = uint160.One, IsPrivateData = false, Key = new byte[] {0xBB},
Value = new byte[] {0xCC}
}
}
}
};

this.proposalResponses = new List<SignedProposalResponse>
{
new SignedProposalResponse
{
ProposalResponse = response,
Endorsement = new Endorsement.Endorsement(this.key.Sign(response.GetHash()).ToDER(), this.key.PubKey.ToBytes())
}
};
}

[Fact]
public void Proposer_Signs_Transaction()
{
var endorsementSigner = new Mock<IEndorsementSigner>();

var builder = new EndorsedTransactionBuilder(endorsementSigner.Object);

Transaction tx = builder.Build(this.proposalResponses);

// We don't need to verify the tx here, the endorsement signer tests take care of that already.
endorsementSigner.Verify(s => s.Sign(tx), Times.Once);
}

[Fact]
public void First_Output_Uses_OpReadWrite()
{
var endorsementSigner = new Mock<IEndorsementSigner>();

var builder = new EndorsedTransactionBuilder(endorsementSigner.Object);

Transaction tx = builder.Build(this.proposalResponses);

Assert.True(tx.Outputs[0].ScriptPubKey.IsReadWriteSet());
}

[Fact]
public void Second_Output_Is_Endorsement()
{
var endorsementSigner = new Mock<IEndorsementSigner>();

var builder = new EndorsedTransactionBuilder(endorsementSigner.Object);

Transaction tx = builder.Build(this.proposalResponses);

Assert.True(tx.Outputs.Count > 1);

var endorsementData = tx.Outputs[1].ScriptPubKey.ToBytes();

var endorsement = Endorsement.Endorsement.FromBytes(endorsementData);

Assert.NotNull(endorsement);
Assert.True(endorsement.Signature.SequenceEqual(this.proposalResponses[0].Endorsement.Signature));
Assert.True(endorsement.PubKey.SequenceEqual(this.proposalResponses[0].Endorsement.PubKey));
}

[Fact]
public void First_Output_Contains_Correct_Data()
{
var endorsementSigner = new Mock<IEndorsementSigner>();

var builder = new EndorsedTransactionBuilder(endorsementSigner.Object);

Transaction tx = builder.Build(this.proposalResponses);

// Expect the data to include the generated RWS, and endorsements
// First op should be OP_READWRITE, second op should be raw data
var rwsData = tx.Outputs[0].ScriptPubKey.ToBytes().Skip(1).ToArray();

var rws = ReadWriteSet.FromJsonEncodedBytes(rwsData);

Assert.NotNull(rws);
Assert.True(rwsData.SequenceEqual(this.proposalResponses[0].ProposalResponse.ReadWriteSet.ToJsonEncodedBytes()));
}
}
}
32 changes: 32 additions & 0 deletions src/Stratis.Feature.PoA.Tokenless.Tests/EndorsementInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ public void MofNPolicyValidator_Validates_One_Signature_One_Organisation_Correct
validator.AddSignature(org, "test");

Assert.True(validator.Valid);
Assert.Single(validator.GetValidAddresses());
}

[Fact]
Expand All @@ -184,6 +185,8 @@ public void MofNPolicyValidator_Validates_Multiple_Signatures_One_Organisation_C
validator.AddSignature(org, "test2");

Assert.True(validator.Valid);
Assert.Equal("test", validator.GetValidAddresses()[0]);
Assert.Equal("test2", validator.GetValidAddresses()[1]);
}

[Fact]
Expand Down Expand Up @@ -214,6 +217,35 @@ public void MofNPolicyValidator_Validates_Multiple_Signatures_Multiple_Organisat
Assert.True(validator.Valid);
}

[Fact]
public void MofNPolicyValidator_Returns_Valid_Addresses_Correctly()
{
var org = (Organisation)"Test";
var org2 = (Organisation)"Test2";
var policy = new Dictionary<Organisation, int>
{
{ org, 2 },
};

var validator = new MofNPolicyValidator(policy);

// Add org 2 signatures - they don't contribute to the policy being valid
validator.AddSignature(org2, "test2 2");
validator.AddSignature(org2, "test2 3");
validator.AddSignature(org2, "test2 4");
Assert.False(validator.Valid);

// Add org 1 signatures
validator.AddSignature(org, "test");
validator.AddSignature(org, "test 2");

Assert.True(validator.Valid);

// Ensure only org 1 addresses are returned
Assert.Equal("test", validator.GetValidAddresses()[0]);
Assert.Equal("test 2", validator.GetValidAddresses()[1]);
}


[Fact]
public void Signed_Proposal_Response_Roundtrip()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Consensus.Rules;
Expand All @@ -15,23 +16,45 @@ public override Task RunAsync(RuleContext context)
{
foreach (Transaction transaction in context.ValidationContext.BlockToValidate.Transactions)
{
for(var i = 0; i < transaction.Outputs.Count; i++)
{
var output = transaction.Outputs[i];
if (!output.ScriptPubKey.IsReadWriteSet())
continue;
if (transaction.Outputs.Count == 0)
continue;

var output = transaction.Outputs[0];
if (!output.ScriptPubKey.IsReadWriteSet())
continue;

// No more outputs to check.
if (i + 1 == transaction.Outputs.Count)
new ConsensusError("badly-formed-rws", "An OP_READWRITE must be followed by an endorsement").Throw();
// Must have 2 or more outputs if OP_RWS
if(transaction.Outputs.Count < 2)
new ConsensusError("badly-formed-rws", "An OP_READWRITE must be followed by an endorsement").Throw();

// TODO check that i + 1 is a signature format
for (var i = 1; i < transaction.Outputs.Count; i++)
{
// Validate endorsement format
if(!ValidateEndorsement(transaction.Outputs[i].ScriptPubKey.ToBytes()))
{
new ConsensusError("badly-formed-endorsement", "Endorsement was not in the correct format.").Throw();
}
}
}

return Task.CompletedTask;
}

private static bool ValidateEndorsement(byte[] toBytes)
{
try
{
Endorsement.Endorsement.FromBytes(toBytes);

return true;
}
catch
{
return false;
}
}
}

/// <summary>
/// Checks that smart contract transactions are in a valid format and the data is serialized correctly.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NBitcoin;
using Stratis.Feature.PoA.Tokenless.Consensus;
using Stratis.SmartContracts.Core.ReadWrite;

namespace Stratis.Feature.PoA.Tokenless.Endorsement
{
public interface IEndorsedTransactionBuilder
{
Transaction Build(IReadOnlyList<SignedProposalResponse> proposalResponses);
}

/// <summary>
/// Builds a transaction that has satisfied an endorsement policy.
/// </summary>
public class EndorsedTransactionBuilder : IEndorsedTransactionBuilder
{
private readonly IEndorsementSigner endorsementSigner;

public EndorsedTransactionBuilder(IEndorsementSigner endorsementSigner)
{
this.endorsementSigner = endorsementSigner;
}

public Transaction Build(IReadOnlyList<SignedProposalResponse> proposalResponses)
{
if (!ValidateProposalResponses(proposalResponses))
return null;

var transaction = new Transaction();

// Endorsement signer uses the same logic to sign the first input with the transaction signing key.
this.endorsementSigner.Sign(transaction);

// TODO at the moment this is the full RWS. We should check that only the public RWS is signed and returned by the endorser.
AddReadWriteSet(transaction, proposalResponses);
AddEndorsements(transaction, proposalResponses.Select(p => p.Endorsement));

return transaction;
}

private static void AddEndorsements(Transaction transaction, IEnumerable<Endorsement> endorsements)
{
foreach(Endorsement endorsement in endorsements)
{
transaction.Outputs.Add(new TxOut(Money.Zero, new Script(endorsement.ToJson())));
}
}

private static void AddReadWriteSet(Transaction transaction, IEnumerable<SignedProposalResponse> proposalResponses)
{
// We can pick any RWS here as they should all be the same
ReadWriteSet rws = GetReadWriteSet(proposalResponses);

Script rwsScriptPubKey = TxReadWriteDataTemplate.Instance.GenerateScriptPubKey(rws.ToJsonEncodedBytes());

transaction.Outputs.Add(new TxOut(Money.Zero, rwsScriptPubKey));
}

private static bool ValidateProposalResponses(IReadOnlyList<SignedProposalResponse> proposalResponses)
{
// Nothing to compare against.
if (proposalResponses.Count < 2) return true;

var serializedProposalResponses = proposalResponses
.Select(r => r.ProposalResponse.ToBytes())
.ToList();

// All elements should be the same.
return serializedProposalResponses.All(b => b.SequenceEqual(serializedProposalResponses[0]));
}

private static ReadWriteSet GetReadWriteSet(IEnumerable<SignedProposalResponse> proposalResponses)
{
return proposalResponses.First().ProposalResponse.ReadWriteSet;
}
}
}
18 changes: 18 additions & 0 deletions src/Stratis.Feature.PoA.Tokenless/Endorsement/EndorsementInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using CertificateAuthority;
using MembershipServices;
using NBitcoin;
Expand All @@ -14,6 +15,7 @@ public class EndorsementInfo
private readonly ICertificatePermissionsChecker permissionsChecker;
private readonly Network network;
private readonly MofNPolicyValidator validator;
private readonly Dictionary<string, SignedProposalResponse> signedProposals;

/// <summary>
/// A basic policy definining a minimum number of endorsement signatures required for an organisation.
Expand All @@ -29,6 +31,11 @@ public EndorsementInfo(Dictionary<Organisation, int> policy, IOrganisationLookup
this.network = network;
this.Policy = policy;
this.validator = new MofNPolicyValidator(this.Policy);

// To prevent returning proposals that were signed correctly but do not match the policy,
// we should keep track of signed proposals from all addresses and filter them by the
// valid addresses in the policy.
this.signedProposals = new Dictionary<string, SignedProposalResponse>();
}

public bool AddSignature(X509Certificate certificate, SignedProposalResponse signedProposalResponse)
Expand All @@ -49,9 +56,20 @@ public bool AddSignature(X509Certificate certificate, SignedProposalResponse sig

AddSignature(org, sender);

this.signedProposals[sender] = signedProposalResponse;

return true;
}

public IReadOnlyList<SignedProposalResponse> GetValidProposalResponses()
{
// Returns signed proposal responses that match current proposals returned by addresses that meet the policy,
return this.validator.GetValidAddresses()
.Where(a => this.signedProposals.ContainsKey(a))
.Select(a => this.signedProposals[a])
.ToList();
}

public void AddSignature(Organisation org, string address)
{
this.validator.AddSignature(org, address);
Expand Down
Loading

0 comments on commit 2d770fb

Please sign in to comment.