Skip to content

Commit

Permalink
AnyoneCanPay support
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Nov 9, 2014
1 parent e661509 commit bf22ad9
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 22 deletions.
94 changes: 94 additions & 0 deletions NBitcoin.Tests/transaction_tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,100 @@ public void CanBuildIssueColoredCoinWithMultiSigP2SH()
Assert.True(builder.Verify(tx));
}

[Fact]
[Trait("UnitTest", "UnitTest")]
//https://github.com/NicolasDorier/NBitcoin/issues/34
public void CanBuildAnyoneCanPayTransaction()
{
//Carla is buying from Alice. Bob is acting as a mediator between Alice and Carla.
var aliceKey = new Key();
var bobKey = new Key();
var carlaKey = new Key();

// Alice + Bob 2 of 2 multisig "wallet"
var aliceBobRedeemScript = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(2, new PubKey[] { aliceKey.PubKey, bobKey.PubKey });

var txBuilder = new TransactionBuilder();
var funding = txBuilder
.AddCoins(GetCoinSource(aliceKey))
.AddKeys(aliceKey)
.Send(aliceBobRedeemScript.ID, "0.5")
.SetChange(aliceKey.PubKey.ID)
.SendFees(Money.Dust)
.BuildTransaction(true);

Assert.True(txBuilder.Verify(funding));

List<ICoin> aliceBobCoins = new List<ICoin>();
aliceBobCoins.Add(new ScriptCoin(funding, funding.Outputs.To(aliceBobRedeemScript.ID).First(), aliceBobRedeemScript));

// first Bob constructs the TX
txBuilder = new TransactionBuilder();
var unsigned = txBuilder
// spend from the Alice+Bob wallet to Carla
.AddCoins(aliceBobCoins)
.Send(carlaKey.PubKey.ID, "0.01")
//and Carla pays Alice
.Send(aliceKey.PubKey.ID, "0.02")
.CoverOnly("0.02")
.SetChange(aliceBobRedeemScript.ID)
// Bob does not sign anything yet
.BuildTransaction(false);

Assert.True(unsigned.Outputs.Count == 3);
Assert.True(unsigned.Outputs[0].IsTo(aliceBobRedeemScript.ID));
//Only 0.02 should be covered, not 0.03 so 0.48 goes back to Alice+Bob
Assert.True(unsigned.Outputs[0].Value == Money.Parse("0.48"));


Assert.True(unsigned.Outputs[1].IsTo(carlaKey.PubKey.ID));
Assert.True(unsigned.Outputs[1].Value == Money.Parse("0.01"));

Assert.True(unsigned.Outputs[2].IsTo(aliceKey.PubKey.ID));
Assert.True(unsigned.Outputs[2].Value == Money.Parse("0.02"));

//Alice signs
txBuilder = new TransactionBuilder();
var aliceSigned = txBuilder
.AddCoins(aliceBobCoins)
.AddKeys(aliceKey)
.SignTransaction(unsigned, SigHash.All | SigHash.AnyoneCanPay);

var carlaCoins = GetCoinSource(carlaKey, "1.0", "0.8", "0.6", "0.2", "0.04");
//Carla fills and signs
txBuilder = new TransactionBuilder();
var carlaSigned = txBuilder
.AddCoins(aliceBobCoins)
.Then()
.AddKeys(carlaKey)
//Carla should complete 0.01, but with 0.03 of fees, she should have a coins of 0.04
.AddCoins(carlaCoins)
.SendFees("0.03")
.CompleteTransaction(aliceSigned)
.BuildTransaction(true);


//Bob review and signs
txBuilder = new TransactionBuilder();
var bobSigned = txBuilder
.AddCoins(aliceBobCoins)
.AddKeys(bobKey)
.SignTransaction(carlaSigned);

txBuilder.AddCoins(carlaCoins);
Assert.True(txBuilder.Verify(bobSigned));
}

private ICoin[] GetCoinSource(Key destination, params Money[] amounts)
{
if(amounts.Length == 0)
amounts = new[] { Money.Parse("100.0") };

return amounts
.Select(a => new Coin(RandOutpoint(), new TxOut(a, destination.PubKey.ID)))
.ToArray();
}

[Fact]
[Trait("UnitTest", "UnitTest")]
public void CanBuildColoredTransaction()
Expand Down
152 changes: 130 additions & 22 deletions NBitcoin/TransactionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ public List<Key> AdditionalKeys
return _AdditionalKeys;
}
}

public SigHash SigHash
{
get;
set;
}
}
internal class TransactionBuildingContext
{
Expand Down Expand Up @@ -209,6 +215,15 @@ public Money AdditionalFees
set;
}

private readonly List<Builder> _AdditionalBuilders = new List<Builder>();
public List<Builder> AdditionalBuilders
{
get
{
return _AdditionalBuilders;
}
}

ColorMarker _Marker;

public ColorMarker GetColorMarker(bool issuance)
Expand Down Expand Up @@ -273,6 +288,18 @@ public bool NonFinalSequenceSet
get;
set;
}

public Money CoverOnly
{
get;
set;
}

public Money Dust
{
get;
set;
}
}

internal class BuilderGroup
Expand Down Expand Up @@ -307,6 +334,12 @@ private void Shuffle(List<Builder> builders)
{
DefaultCoinSelector.Shuffle(builders, _Parent._Rand);
}

public Money CoverOnly
{
get;
set;
}
}

List<BuilderGroup> _BuilderGroups = new List<BuilderGroup>();
Expand Down Expand Up @@ -587,13 +620,20 @@ public TransactionBuilder SetCoinSelector(ICoinSelector selector)
}

public Transaction BuildTransaction(bool sign)
{
return BuildTransaction(sign, SigHash.All);
}
public Transaction BuildTransaction(bool sign, SigHash sigHash)
{
TransactionBuildingContext ctx = new TransactionBuildingContext(this);
if(_CompletedTransaction != null)
ctx.Transaction = _CompletedTransaction;
if(_LockTime != null && _LockTime.HasValue)
ctx.Transaction.LockTime = _LockTime.Value;
foreach(var group in _BuilderGroups)
{
ctx.Group = group;
ctx.AdditionalBuilders.Clear();
ctx.AdditionalFees = Money.Zero;

foreach(var builder in group.IssuanceBuilders)
Expand All @@ -604,50 +644,56 @@ public Transaction BuildTransaction(bool sign)
{
var coins = group.Coins.OfType<ColoredCoin>().Where(c => c.Asset.Id == builders.Key).OfType<ICoin>();

var btcSpent = BuildTransaction(ctx, builders.Value, coins, group.ChangeScript, Money.Zero)
ctx.Dust = Money.Zero;
ctx.CoverOnly = null;
var btcSpent = BuildTransaction(ctx, group, builders.Value, coins)
.OfType<IColoredCoin>().Select(c => c.Bearer.Amount).Sum();
ctx.AdditionalFees -= btcSpent;
}

var coinBuilders = group.Builders.ToList();
coinBuilders.Add(_ => _.AdditionalFees);
BuildTransaction(ctx, coinBuilders, group.Coins.OfType<Coin>(), group.ChangeScript, Money.Dust);
ctx.AdditionalBuilders.Add(_ => _.AdditionalFees);
ctx.Dust = Money.Dust;
ctx.CoverOnly = group.CoverOnly;
BuildTransaction(ctx, group, group.Builders, group.Coins.OfType<Coin>());
}
ctx.Finish();

if(sign)
{
SignTransactionInPlace(ctx.Transaction);
SignTransactionInPlace(ctx.Transaction, sigHash);
}
return ctx.Transaction;
}

private IEnumerable<ICoin> BuildTransaction(
TransactionBuildingContext ctx,
List<Builder> builders,
IEnumerable<ICoin> coins,
Script changeScript,
Money dust)
BuilderGroup group,
IEnumerable<Builder> builders,
IEnumerable<ICoin> coins)
{
var originalCtx = ctx.CreateMemento();
var target = builders.Select(b => b(ctx)).Sum();
var target = builders.Concat(ctx.AdditionalBuilders).Select(b => b(ctx)).Sum();
if(ctx.CoverOnly != null)
{
target = ctx.CoverOnly + ctx.ChangeAmount;
}
var selection = CoinSelector.Select(coins, target);
if(selection == null)
throw new NotEnoughFundsException("Not enough fund to cover the target");
var total = selection.Select(s => s.Amount).Sum();
var change = total - target;
if(change < Money.Zero)
throw new NotEnoughFundsException("Not enough fund to cover the target");
if(change > dust)
if(change > ctx.Dust)
{
if(changeScript == null)
if(group.ChangeScript == null)
throw new InvalidOperationException("A change address should be specified");

ctx.RestoreMemento(originalCtx);
ctx.ChangeAmount = change;
try
{
return BuildTransaction(ctx, builders, coins, changeScript, dust);
return BuildTransaction(ctx, group, builders, coins);
}
finally
{
Expand All @@ -665,15 +711,26 @@ public Transaction BuildTransaction(bool sign)
}
return selection;
}
public Transaction SignTransaction(Transaction transaction)

public Transaction SignTransaction(Transaction transaction, SigHash sigHash)
{
var tx = transaction.Clone();
SignTransactionInPlace(tx);
SignTransactionInPlace(tx, sigHash);
return tx;
}

public Transaction SignTransaction(Transaction transaction)
{
return SignTransaction(transaction, SigHash.All);
}
public void SignTransactionInPlace(Transaction transaction)
{
SignTransactionInPlace(transaction, SigHash.All);
}
public void SignTransactionInPlace(Transaction transaction, SigHash sigHash)
{
TransactionSigningContext ctx = new TransactionSigningContext(this, transaction);
ctx.SigHash = sigHash;
for(int i = 0 ; i < transaction.Inputs.Count ; i++)
{
var txIn = transaction.Inputs[i];
Expand All @@ -693,7 +750,7 @@ public bool Verify(Transaction tx, Money expectFees = null)
var txIn = tx.Inputs[i];
var coin = FindCoin(txIn.PrevOut);
if(coin == null)
throw new KeyNotFoundException("Impossible to find the scriptPubKey of outpoint " + txIn.PrevOut);
throw CoinNotFound(txIn);
spent += coin is IColoredCoin ? ((IColoredCoin)coin).Bearer.Amount : coin.Amount;
if(!Script.VerifyScript(txIn.ScriptSig, coin.ScriptPubKey, tx, i))
return false;
Expand All @@ -707,6 +764,11 @@ public bool Verify(Transaction tx, Money expectFees = null)
return true;
}

private Exception CoinNotFound(TxIn txIn)
{
return new KeyNotFoundException("Impossible to find the scriptPubKey of outpoint " + txIn.PrevOut);
}

private ICoin FindCoin(OutPoint outPoint)
{
return _BuilderGroups.SelectMany(c => c.Coins).FirstOrDefault(c => c.Outpoint == outPoint);
Expand Down Expand Up @@ -774,9 +836,9 @@ private Script CreateScriptSig(TransactionSigningContext ctx, TxIn input, ICoin
var key = FindKey(ctx, pubKeyHashParams);
if(key == null)
return originalScriptSig;
var hash = input.ScriptSig.SignatureHash(ctx.Transaction, n, SigHash.All);
var hash = input.ScriptSig.SignatureHash(ctx.Transaction, n, ctx.SigHash);
var sig = key.Sign(hash);
return payToPubKeyHash.GenerateScriptSig(new TransactionSignature(sig, SigHash.All), key.PubKey);
return payToPubKeyHash.GenerateScriptSig(new TransactionSignature(sig, ctx.SigHash), key.PubKey);
}

var multiSigParams = payToMultiSig.ExtractScriptPubKeyParameters(scriptPubKey);
Expand Down Expand Up @@ -812,9 +874,9 @@ private Script CreateScriptSig(TransactionSigningContext ctx, TxIn input, ICoin
}
if(keys[i] != null)
{
var hash = input.ScriptSig.SignatureHash(ctx.Transaction, n, SigHash.All);
var hash = input.ScriptSig.SignatureHash(ctx.Transaction, n, ctx.SigHash);
var sig = keys[i].Sign(hash);
signatures[i] = new TransactionSignature(sig, SigHash.All);
signatures[i] = new TransactionSignature(sig, ctx.SigHash);
sigCount++;
}
}
Expand All @@ -834,9 +896,9 @@ private Script CreateScriptSig(TransactionSigningContext ctx, TxIn input, ICoin
var key = FindKey(ctx, pubKeyParams);
if(key == null)
return originalScriptSig;
var hash = input.ScriptSig.SignatureHash(ctx.Transaction, n, SigHash.All);
var hash = input.ScriptSig.SignatureHash(ctx.Transaction, n, ctx.SigHash);
var sig = key.Sign(hash);
return payToPubKey.GenerateScriptSig(new TransactionSignature(sig, SigHash.All));
return payToPubKey.GenerateScriptSig(new TransactionSignature(sig, ctx.SigHash));
}

throw new NotSupportedException("Unsupported scriptPubKey");
Expand Down Expand Up @@ -867,5 +929,51 @@ public TransactionBuilder Send(PubKey pubKey, Money amount)
{
return Send(pubKey.PaymentScript, amount);
}

/// <summary>
/// Specify the amount of money to cover txouts, if not specified all txout will be covered
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
public TransactionBuilder CoverOnly(Money amount)
{
CurrentGroup.CoverOnly = amount;
return this;
}


Transaction _CompletedTransaction;

/// <summary>
/// Find Coins to complete TxOut of the transaction (Does not support colored coins)
/// </summary>
/// <param name="transaction"></param>
/// <returns></returns>
public TransactionBuilder CompleteTransaction(Transaction transaction)
{
if(_CompletedTransaction != null)
throw new InvalidOperationException("Transaction to complete already set");
_CompletedTransaction = transaction.Clone();

var spent = transaction.Inputs.Select(txin =>
{
var c = FindCoin(txin.PrevOut);
if(c == null)
throw CoinNotFound(txin);
if(c is IColoredCoin)
return null;
return c;
})
.Where(c => c != null)
.Select(c => c.Amount)
.Sum();

var toComplete = transaction.TotalOut - spent;
CurrentGroup.Builders.Add(ctx =>
{
return toComplete;
});
return this;
}
}
}

5 comments on commit bf22ad9

@dthorpe
Copy link
Contributor

Choose a reason for hiding this comment

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

When Bob is creating the multiparty transaction, the code shows a payment of 0.01 from the Alice+Bob wallet to Carla, and a payment of 0.02 to Alice, to be paid by Carla.

However, CoverOnly("0.02") seems to indicate that 0.02 is being paid out of the Alice+Bob wallet. Isn't that incorrect? The Alice+Bob wallet only needs to cover 0.01, the payment to Carla.

@dthorpe
Copy link
Contributor

Choose a reason for hiding this comment

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

var carlaSigned = txBuilder
                .AddCoins(aliceBobCoins)
                .Then()

Carla has to know Alice+Bob's coin sources? Why? The Alice+Bob inputs are already specified/resolved in the aliceSigned tx.

And Carla knows Alice+Bob's redeem script? That seems worrisome.

If Carla knows the Alice+Bob redeem script, can't Carla use that to spend from the Alice+Bob tx without Alice or Bob's consent/signature?

@NicolasDorier
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Carla has to know Alice+Bob's coin sources? Why? The Alice+Bob inputs are already specified/resolved in the aliceSigned tx. 

So she knows how much coin she has to add to cover the transaction.

And Carla knows Alice+Bob's redeem script? That seems worrisome.

Carla does not need to know the Redeem, if she has a normal Coin, it is enough. (Only the Amount is important)

If Carla knows the Alice+Bob redeem script, can't Carla use that to spend from the Alice+Bob tx without Alice or Bob's consent/signature?

Carla can't spend without consent, even if she had the ScriptCoin (which I is not mandatory), because Bob have not signed yet.

However, CoverOnly("0.02") seems to indicate that 0.02 is being paid out of the Alice+Bob wallet. Isn't that incorrect? The Alice+Bob wallet only needs to cover 0.01, the payment to Carla.

I'll modify later

@dthorpe
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer that Bob tells Carla how much Carla needs to contribute to the transaction, since there could be multiple additional sources that have not been specified yet. If Carla and Dan need to contribute to the transaction, then Carla knowing how much Alice+Bob have already contributed isn't enough for Carla to know (from the tx alone) how much she is to contribute. The amount to contribute has to be communicated outside the partial transaction being passed around.

@NicolasDorier
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Take a look at 3524dfd
I found a clean way to allow both scenario

Please sign in to comment.