Commit 398ec79f authored by Jeremy Bokobza's avatar Jeremy Bokobza Committed by GitHub

Merge pull request #98 from bokobza/master

TumbleBit integration work
parents 065a6810 91e9a8a9
...@@ -63,7 +63,7 @@ namespace Breeze.TumbleBit.Controllers ...@@ -63,7 +63,7 @@ namespace Breeze.TumbleBit.Controllers
try try
{ {
await this.tumbleBitManager.TumbleAsync(request.DestinationWalletName); await this.tumbleBitManager.TumbleAsync(request.OriginWalletName, request.DestinationWalletName);
return this.Ok(); return this.Ok();
} }
catch (Exception e) catch (Exception e)
......
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using NTumbleBit;
namespace Breeze.TumbleBit.Client
{
public class ExternalServices
{
public Transaction FundTransaction(TxOut txOut, FeeRate feeRate)
{
return null;
}
public void Track(Script scriptPubkey)
{
}
public bool Broadcast(Transaction tx)
{
return true;
}
public void TrustedBroadcast(int cycleStart, TransactionType transactionType, uint correlation, TrustedBroadcastRequest broadcast)
{
}
public TransactionInformation[] GetTransactions(Script scriptPubKey, bool withProof)
{
return null;
}
public FeeRate GetFeeRate()
{
return null;
}
}
}
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
void Save(); void Save();
/// <summary> /// <summary>
/// Loads the state of the current tumbling session. /// Loads the saved state of the tumbling execution to the file system.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
IStateMachine Load(); void LoadStateFromMemory();
/// <summary> /// <summary>
/// Deletes the state of the current tumbling session.. /// Deletes the state of the current tumbling session..
......
...@@ -17,7 +17,7 @@ namespace Breeze.TumbleBit.Client ...@@ -17,7 +17,7 @@ namespace Breeze.TumbleBit.Client
/// <returns></returns> /// <returns></returns>
Task<ClassicTumblerParameters> ConnectToTumblerAsync(Uri serverAddress); Task<ClassicTumblerParameters> ConnectToTumblerAsync(Uri serverAddress);
Task TumbleAsync(string destinationWalletName); Task TumbleAsync(string originWalletName, string destinationWalletName);
/// <summary> /// <summary>
/// Processes a block received from the network. /// Processes a block received from the network.
......
...@@ -40,5 +40,7 @@ namespace Breeze.TumbleBit.Client ...@@ -40,5 +40,7 @@ namespace Breeze.TumbleBit.Client
Task<PuzzleSolver.ServerCommitment[]> SolvePuzzlesAsync(int cycleId, string channelId, PuzzleValue[] puzzles); Task<PuzzleSolver.ServerCommitment[]> SolvePuzzlesAsync(int cycleId, string channelId, PuzzleValue[] puzzles);
Task<SolutionKey[]> FulfillOfferAsync(int cycleId, string channelId, TransactionSignature clientSignature); Task<SolutionKey[]> FulfillOfferAsync(int cycleId, string channelId, TransactionSignature clientSignature);
Task GiveEscapeKeyAsync(int cycleId, string channelId, TransactionSignature signature);
} }
} }
...@@ -28,7 +28,10 @@ namespace Breeze.TumbleBit.Models ...@@ -28,7 +28,10 @@ namespace Breeze.TumbleBit.Models
public class TumbleRequest public class TumbleRequest
{ {
[Required(ErrorMessage = "A wallet name is required.")] [Required(ErrorMessage = "The name of the origin wallet is required.")]
public string OriginWalletName { get; set; }
[Required(ErrorMessage = "The name of the destination wallet is required.")]
public string DestinationWalletName { get; set; } public string DestinationWalletName { get; set; }
} }
} }
This diff is collapsed.
namespace Breeze.TumbleBit.Client
{
public enum TransactionType : int
{
TumblerEscrow,
TumblerRedeem,
/// <summary>
/// The transaction that cashout tumbler's escrow (go to client)
/// </summary>
TumblerCashout,
ClientEscrow,
ClientRedeem,
ClientOffer,
ClientEscape,
/// <summary>
/// The transaction that cashout client's escrow (go to tumbler)
/// </summary>
ClientFulfill,
ClientOfferRedeem
}
}
...@@ -23,21 +23,18 @@ namespace Breeze.TumbleBit.Client ...@@ -23,21 +23,18 @@ namespace Breeze.TumbleBit.Client
private readonly Network network; private readonly Network network;
private TumblingState tumblingState; private TumblingState tumblingState;
private IDisposable blockReceiver; private IDisposable blockReceiver;
int lastCycleStarted;
private ClassicTumblerParameters TumblerParameters { get; set; } private ClassicTumblerParameters TumblerParameters { get; set; }
public TumbleBitManager(ILoggerFactory loggerFactory, IWalletManager walletManager, ConcurrentChain chain, Network network, Signals signals) public TumbleBitManager(ILoggerFactory loggerFactory, IWalletManager walletManager, ConcurrentChain chain, Network network, Signals signals)
{ {
this.lastCycleStarted = 0;
this.walletManager = walletManager; this.walletManager = walletManager;
this.chain = chain; this.chain = chain;
this.signals = signals; this.signals = signals;
this.network = network; this.network = network;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName); this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
// load the persisted tumbling state this.tumblingState = new TumblingState(loggerFactory, this.chain, this.walletManager, this.network);
this.tumblingState = TumblingState.LoadState();
} }
/// <inheritdoc /> /// <inheritdoc />
...@@ -50,21 +47,21 @@ namespace Breeze.TumbleBit.Client ...@@ -50,21 +47,21 @@ namespace Breeze.TumbleBit.Client
{ {
throw new Exception($"The tumbler is on network {this.TumblerParameters.Network} while the wallet is on network {this.network}."); throw new Exception($"The tumbler is on network {this.TumblerParameters.Network} while the wallet is on network {this.network}.");
} }
if (this.tumblingState == null) // load the current tumbling state fromt he file system
{ this.tumblingState.LoadStateFromMemory();
this.tumblingState = new TumblingState();
}
// update and save the state // update and save the state
this.tumblingState.TumblerUri = serverAddress;
this.tumblingState.TumblerParameters = this.TumblerParameters; this.tumblingState.TumblerParameters = this.TumblerParameters;
this.tumblingState.SetClients(this.tumblerService);
this.tumblingState.Save(); this.tumblingState.Save();
return this.TumblerParameters; return this.TumblerParameters;
} }
/// <inheritdoc /> /// <inheritdoc />
public Task TumbleAsync(string destinationWalletName) public Task TumbleAsync(string originWalletName, string destinationWalletName)
{ {
// make sure the tumbler service is initialized // make sure the tumbler service is initialized
if (this.TumblerParameters == null || this.tumblerService == null) if (this.TumblerParameters == null || this.tumblerService == null)
...@@ -81,11 +78,21 @@ namespace Breeze.TumbleBit.Client ...@@ -81,11 +78,21 @@ namespace Breeze.TumbleBit.Client
Wallet destinationWallet = this.walletManager.GetWallet(destinationWalletName); Wallet destinationWallet = this.walletManager.GetWallet(destinationWalletName);
if (destinationWallet == null) if (destinationWallet == null)
{ {
throw new Exception($"Destination not found. Have you created a wallet with name {destinationWalletName}?"); throw new Exception($"Destination wallet not found. Have you created a wallet with name {destinationWalletName}?");
}
Wallet originWallet = this.walletManager.GetWallet(originWalletName);
if (originWallet == null)
{
throw new Exception($"Origin wallet not found. Have you created a wallet with name {originWalletName}?");
} }
// update the state and save // update the state and save
this.tumblingState.DestinationWallet = destinationWallet;
this.tumblingState.DestinationWalletName = destinationWalletName; this.tumblingState.DestinationWalletName = destinationWalletName;
this.tumblingState.OriginWallet = originWallet;
this.tumblingState.OriginWalletName = originWalletName;
this.tumblingState.Save(); this.tumblingState.Save();
// subscribe to receiving blocks // subscribe to receiving blocks
...@@ -113,30 +120,13 @@ namespace Breeze.TumbleBit.Client ...@@ -113,30 +120,13 @@ namespace Breeze.TumbleBit.Client
/// <inheritdoc /> /// <inheritdoc />
public void ProcessBlock(int height, Block block) public void ProcessBlock(int height, Block block)
{ {
// TODO start the state machine this.logger.LogDebug($"Received block with height {height} during tumbling session.");
this.logger.LogDebug($"Receive block with height {height}");
// update the block height in the tumbling state // update the block height in the tumbling state
this.tumblingState.LastBlockReceivedHeight = height; this.tumblingState.LastBlockReceivedHeight = height;
this.tumblingState.Save(); this.tumblingState.Save();
// get the next cycle to be started
var cycle = this.TumblerParameters.CycleGenerator.GetRegistratingCycle(height);
// check if we need to start a new session starting from the registration cycle
if (this.lastCycleStarted != cycle.Start)
{
this.lastCycleStarted = cycle.Start;
this.logger.LogDebug($"new registration cycle at {cycle.Start}");
if (this.tumblingState.Sessions.SingleOrDefault(s => s.StartCycle == cycle.Start) == null)
{
this.tumblingState.CreateNewSession(cycle.Start);
this.logger.LogDebug($"new session created at {cycle.Start}");
}
}
// update the state of the tumbling session in this new block // update the state of the tumbling session in this new block
this.tumblingState.Update(); this.tumblingState.Update();
} }
......
...@@ -110,5 +110,11 @@ namespace Breeze.TumbleBit.Client ...@@ -110,5 +110,11 @@ namespace Breeze.TumbleBit.Client
SolutionKey[] result = await this.serverAddress.AppendPathSegment($"api/v1/tumblers/0/clientchannels/{cycleId}/{channelId}/offer").PostJsonAsync(clientSignature).ReceiveJson<SolutionKey[]>(); SolutionKey[] result = await this.serverAddress.AppendPathSegment($"api/v1/tumblers/0/clientchannels/{cycleId}/{channelId}/offer").PostJsonAsync(clientSignature).ReceiveJson<SolutionKey[]>();
return result; return result;
} }
/// <inheritdoc />
public async Task GiveEscapeKeyAsync(int cycleId, string channelId, TransactionSignature signature)
{
await this.serverAddress.AppendPathSegment($"api/v1/tumblers/0/clientchannels/{cycleId}/{channelId}/escape").PostJsonAsync(signature);
}
} }
} }
\ No newline at end of file
...@@ -4,34 +4,74 @@ using System.IO; ...@@ -4,34 +4,74 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NBitcoin;
using Newtonsoft.Json; using Newtonsoft.Json;
using NTumbleBit.ClassicTumbler; using NTumbleBit.ClassicTumbler;
using NTumbleBit.PuzzlePromise; using NTumbleBit.PuzzlePromise;
using NTumbleBit.PuzzleSolver; using NTumbleBit.PuzzleSolver;
using Stratis.Bitcoin.Wallet;
namespace Breeze.TumbleBit.Client namespace Breeze.TumbleBit.Client
{ {
public class TumblingState : IStateMachine public partial class TumblingState : IStateMachine
{ {
private const string StateFileName = "tumblebit_state.json"; private const string StateFileName = "tumblebit_state.json";
private readonly ILogger logger;
private readonly ConcurrentChain chain;
private readonly IWalletManager walletManager;
private readonly CoinType coinType;
[JsonProperty("tumblerParameters")] [JsonProperty("tumblerParameters")]
public ClassicTumblerParameters TumblerParameters { get; set; } public ClassicTumblerParameters TumblerParameters { get; set; }
[JsonProperty("tumblerUri")] [JsonProperty("tumblerUri")]
public Uri TumblerUri { get; set; } public Uri TumblerUri { get; set; }
[JsonProperty("lastBlockReceivedHeight")] [JsonProperty("lastBlockReceivedHeight", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int LastBlockReceivedHeight { get; set; } public int LastBlockReceivedHeight { get; set; }
[JsonProperty("destinationWalletName")] [JsonProperty("originWalletName", NullValueHandling = NullValueHandling.Ignore)]
public string OriginWalletName { get; set; }
[JsonProperty("destinationWalletName", NullValueHandling = NullValueHandling.Ignore)]
public string DestinationWalletName { get; set; } public string DestinationWalletName { get; set; }
[JsonProperty("sessions", NullValueHandling = NullValueHandling.Ignore)]
public IList<Session> Sessions { get; set; } public IList<Session> Sessions { get; set; }
[JsonIgnore]
public Wallet OriginWallet { get; set; }
[JsonIgnore]
public Wallet DestinationWallet { get; set; }
[JsonIgnore]
public ITumblerService AliceClient { get; set; }
[JsonIgnore]
public ITumblerService BobClient { get; set; }
[JsonConstructor]
public TumblingState() public TumblingState()
{ {
this.Sessions = new List<Session>(); }
public TumblingState(ILoggerFactory loggerFactory,
ConcurrentChain chain,
IWalletManager walletManager,
Network network)
{
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.chain = chain;
this.walletManager = walletManager;
this.coinType = (CoinType)network.Consensus.CoinType;
}
public void SetClients(ITumblerService tumblerService)
{
this.AliceClient = tumblerService;
this.BobClient = tumblerService;
} }
/// <inheritdoc /> /// <inheritdoc />
...@@ -41,9 +81,23 @@ namespace Breeze.TumbleBit.Client ...@@ -41,9 +81,23 @@ namespace Breeze.TumbleBit.Client
} }
/// <inheritdoc /> /// <inheritdoc />
public IStateMachine Load() public void LoadStateFromMemory()
{ {
return LoadState(); var stateFilePath = GetStateFilePath();
if (!File.Exists(stateFilePath))
{
return;
}
// load the file from the local system
var savedState = JsonConvert.DeserializeObject<TumblingState>(File.ReadAllText(stateFilePath));
this.Sessions = savedState.Sessions ?? new List<Session>();
this.OriginWalletName = savedState.OriginWalletName;
this.DestinationWalletName = savedState.DestinationWalletName;
this.LastBlockReceivedHeight = savedState.LastBlockReceivedHeight;
this.TumblerParameters = savedState.TumblerParameters;
this.TumblerUri = savedState.TumblerUri;
} }
/// <inheritdoc /> /// <inheritdoc />
...@@ -56,67 +110,70 @@ namespace Breeze.TumbleBit.Client ...@@ -56,67 +110,70 @@ namespace Breeze.TumbleBit.Client
/// <inheritdoc /> /// <inheritdoc />
public void Update() public void Update()
{ {
// get the next cycle to be started
var cycle = this.TumblerParameters.CycleGenerator.GetRegistratingCycle(this.LastBlockReceivedHeight);
// create a new session if allowed
if (this.Sessions.Count == 0)
{
this.CreateNewSession(cycle.Start);
}
else
{
// TODO remove the limitation to have only 1 session
//var lastCycleStarted = this.Sessions.Max(s => s.StartCycle);
//// check if we need to start a new session starting from the registration cycle
//if (lastCycleStarted != cycle.Start)
//{
// if (this.Sessions.SingleOrDefault(s => s.StartCycle == cycle.Start) == null)
// {
// this.CreateNewSession(cycle.Start);
// }
//}
}
// get a list of cycles we expect to have at this height // get a list of cycles we expect to have at this height
var cycles = this.TumblerParameters.CycleGenerator.GetCycles(this.LastBlockReceivedHeight); var cycles = this.TumblerParameters.CycleGenerator.GetCycles(this.LastBlockReceivedHeight);
var existingSessions = cycles.SelectMany(c => this.Sessions.Where(s => s.StartCycle == c.Start)).ToList();
var states = cycles.SelectMany(c => this.Sessions.Where(s => s.StartCycle == c.Start)).ToList(); foreach (var existingSession in existingSessions)
foreach (var state in states)
{ {
try // create a new session to be updated
var session = new Session();
if (existingSession.NegotiationClientState != null)
{ {
// create a new session to be updated session.StartCycle = existingSession.NegotiationClientState.CycleStart;
var session = new Session(); session.ClientChannelNegotiation = new ClientChannelNegotiation(this.TumblerParameters, existingSession.NegotiationClientState);
if (state.NegotiationClientState != null)
{
session.StartCycle = state.NegotiationClientState.CycleStart;
session.ClientChannelNegotiation = new ClientChannelNegotiation(this.TumblerParameters, state.NegotiationClientState);
}
if (state.PromiseClientState != null)
session.PromiseClientSession = new PromiseClientSession(this.TumblerParameters.CreatePromiseParamaters(), state.PromiseClientState);
if (state.SolverClientState != null)
session.SolverClientSession = new SolverClientSession(this.TumblerParameters.CreateSolverParamaters(), state.SolverClientState);
// update the session
session.Update();
// replace the updated session in the list of existing sessions
int index = this.Sessions.IndexOf(state);
if (index != -1)
{
this.Sessions[index] = session;
}
this.Save();
} }
catch (Exception) if (existingSession.PromiseClientState != null)
session.PromiseClientSession = new PromiseClientSession(this.TumblerParameters.CreatePromiseParamaters(), existingSession.PromiseClientState);
if (existingSession.SolverClientState != null)
session.SolverClientSession = new SolverClientSession(this.TumblerParameters.CreateSolverParamaters(), existingSession.SolverClientState);
// update the session
this.MoveToNextPhase(session);
// replace the updated session in the list of existing sessions
int index = this.Sessions.IndexOf(existingSession);
if (index != -1)
{ {
throw; this.Sessions[index] = session;
} }
this.Save();
} }
} }
public void MoveToNextPhase(Session session)
{
this.logger.LogInformation($"Entering next phase for cycle {session.StartCycle}.");
}
public void CreateNewSession(int start) public void CreateNewSession(int start)
{ {
this.Sessions.Add(new Session { StartCycle = start }); this.Sessions.Add(new Session { StartCycle = start });
this.Save(); this.Save();
} }
/// <summary>
/// Loads the saved state of the tumbling execution to the file system.
/// </summary>
/// <returns></returns>
public static TumblingState LoadState()
{
var stateFilePath = GetStateFilePath();
if (!File.Exists(stateFilePath))
{
return null;
}
// load the file from the local system
return JsonConvert.DeserializeObject<TumblingState>(File.ReadAllText(stateFilePath));
}
/// <summary> /// <summary>
/// Gets the file path of the file containing the state of the tumbling execution. /// Gets the file path of the file containing the state of the tumbling execution.
...@@ -152,15 +209,13 @@ namespace Breeze.TumbleBit.Client ...@@ -152,15 +209,13 @@ namespace Breeze.TumbleBit.Client
public SolverClientSession.State SolverClientState { get; set; } public SolverClientSession.State SolverClientState { get; set; }
[JsonIgnore]
public ClientChannelNegotiation ClientChannelNegotiation { get; set; } public ClientChannelNegotiation ClientChannelNegotiation { get; set; }
[JsonIgnore]
public SolverClientSession SolverClientSession { get; set; } public SolverClientSession SolverClientSession { get; set; }
[JsonIgnore]
public PromiseClientSession PromiseClientSession { get; set; } public PromiseClientSession PromiseClientSession { get; set; }
public void Update()
{
}
} }
} }
...@@ -71,8 +71,9 @@ namespace NTumbleBit ...@@ -71,8 +71,9 @@ namespace NTumbleBit
tx.Inputs.Add(new TxIn(coin.Outpoint)); tx.Inputs.Add(new TxIn(coin.Outpoint));
tx.Inputs[0].Sequence = 0; tx.Inputs[0].Sequence = 0;
tx.Outputs.Add(new TxOut(coin.Amount, redeemDestination)); tx.Outputs.Add(new TxOut(coin.Amount, redeemDestination));
var vSize = tx.GetVirtualSize() + 80;
tx.Inputs[0].ScriptSig = EscrowScriptBuilder.GenerateScriptSig(new TransactionSignature[] { null }) + Op.GetPushOp(coin.Redeem.ToBytes()); tx.Inputs[0].ScriptSig = EscrowScriptBuilder.GenerateScriptSig(new TransactionSignature[] { null }) + Op.GetPushOp(coin.Redeem.ToBytes());
var vSize = tx.GetVirtualSize() + 80; // Size without signature + the signature size
tx.Outputs[0].Value -= feeRate.GetFee(vSize); tx.Outputs[0].Value -= feeRate.GetFee(vSize);
var redeemTransaction = new TrustedBroadcastRequest var redeemTransaction = new TrustedBroadcastRequest
......
using NBitcoin.RPC; using NBitcoin.RPC;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
...@@ -16,5 +17,33 @@ namespace NTumbleBit ...@@ -16,5 +17,33 @@ namespace NTumbleBit
Params = parameters Params = parameters
}, throwIfRPCError: false); }, throwIfRPCError: false);
} }
public static JArray ListTransactions(this RPCClient rpcClient)
{
JArray array = new JArray();
int count = 100;
int skip = 0;
int highestConfirmation = 0;
while(true)
{
var result = rpcClient.SendCommandNoThrows("listtransactions", "*", count, skip, true);
skip += count;
if(result.Error != null)
return null;
var transactions = (JArray)result.Result;
foreach(var obj in transactions)
{
array.Add(obj);
if(obj["confirmations"] != null)
{
highestConfirmation = Math.Max(highestConfirmation, (int)obj["confirmations"]);
}
}
if(transactions.Count < count || highestConfirmation >= 1400)
break;
}
return array;
}
} }
} }
...@@ -245,7 +245,12 @@ namespace NTumbleBit.PuzzlePromise ...@@ -245,7 +245,12 @@ namespace NTumbleBit.PuzzlePromise
Transaction cashout = new Transaction(); Transaction cashout = new Transaction();
cashout.AddInput(new TxIn(InternalState.EscrowedCoin.Outpoint, Script.Empty)); cashout.AddInput(new TxIn(InternalState.EscrowedCoin.Outpoint, Script.Empty));
cashout.AddOutput(new TxOut(Money.Zero, cashoutDestination)); cashout.AddOutput(new TxOut(Money.Zero, cashoutDestination));
var fee = feeRate.GetFee(cashout.GetVirtualSize());
var tb = new TransactionBuilder();
tb.Extensions.Add(new EscrowBuilderExtension());
tb.AddCoins(InternalState.EscrowedCoin);
var size = tb.EstimateSize(cashout, true);
var fee = feeRate.GetFee(size);
cashout.Outputs[0].Value = InternalState.EscrowedCoin.Amount - fee; cashout.Outputs[0].Value = InternalState.EscrowedCoin.Amount - fee;
......
...@@ -318,6 +318,15 @@ namespace NTumbleBit.PuzzleSolver ...@@ -318,6 +318,15 @@ namespace NTumbleBit.PuzzleSolver
return signature; return signature;
} }
public TransactionSignature SignEscape()
{
AssertState(SolverClientStates.Completed);
var dummy = new Transaction();
dummy.Inputs.Add(new TxIn(InternalState.EscrowedCoin.Outpoint));
dummy.Outputs.Add(new TxOut());
return dummy.SignInput(InternalState.EscrowKey, InternalState.EscrowedCoin, SigHash.None | SigHash.AnyoneCanPay);
}
private Transaction CreateUnsignedOfferTransaction() private Transaction CreateUnsignedOfferTransaction()
{ {
Script offer = CreateOfferScript(); Script offer = CreateOfferScript();
...@@ -333,15 +342,17 @@ namespace NTumbleBit.PuzzleSolver ...@@ -333,15 +342,17 @@ namespace NTumbleBit.PuzzleSolver
{ {
var coin = CreateUnsignedOfferTransaction().Outputs.AsCoins().First().ToScriptCoin(CreateOfferScript()); var coin = CreateUnsignedOfferTransaction().Outputs.AsCoins().First().ToScriptCoin(CreateOfferScript());
var unknownOutpoints = new OutPoint(uint256.Zero, 0);
Transaction tx = new Transaction(); Transaction tx = new Transaction();
tx.LockTime = CreateOfferScriptParameters().Expiration; tx.LockTime = CreateOfferScriptParameters().Expiration;
tx.Inputs.Add(new TxIn(coin.Outpoint)); tx.Inputs.Add(new TxIn(unknownOutpoints));
tx.Inputs[0].Sequence = 0; tx.Inputs[0].Sequence = 0;
tx.Outputs.Add(new TxOut(coin.Amount, redeemDestination)); tx.Outputs.Add(new TxOut(coin.Amount, redeemDestination));
var vSize = tx.GetVirtualSize() + 80;
tx.Outputs[0].Value -= feeRate.GetFee(vSize);
tx.Inputs[0].ScriptSig = new Script(OpcodeType.OP_0) + Op.GetPushOp(coin.Redeem.ToBytes()); tx.Inputs[0].ScriptSig = new Script(OpcodeType.OP_0) + Op.GetPushOp(coin.Redeem.ToBytes());
var vSize = tx.GetVirtualSize() + 80; // Size without signature + the signature size
tx.Outputs[0].Value -= feeRate.GetFee(vSize);
var redeemTransaction = new TrustedBroadcastRequest var redeemTransaction = new TrustedBroadcastRequest
{ {
Key = InternalState.RedeemKey, Key = InternalState.RedeemKey,
...@@ -349,7 +360,7 @@ namespace NTumbleBit.PuzzleSolver ...@@ -349,7 +360,7 @@ namespace NTumbleBit.PuzzleSolver
Transaction = tx Transaction = tx
}; };
//Strip redeem script information so we check if TrustedBroadcastRequest can sign correctly //Strip redeem script information so we check if TrustedBroadcastRequest can sign correctly
redeemTransaction.Transaction = redeemTransaction.ReSign(new Coin(coin.Outpoint, coin.TxOut)); redeemTransaction.Transaction = redeemTransaction.ReSign(new Coin(unknownOutpoints, coin.TxOut));
return redeemTransaction; return redeemTransaction;
} }
......
...@@ -20,6 +20,7 @@ namespace NTumbleBit.PuzzleSolver ...@@ -20,6 +20,7 @@ namespace NTumbleBit.PuzzleSolver
WaitingRevelation, WaitingRevelation,
WaitingBlindFactor, WaitingBlindFactor,
WaitingFulfillment, WaitingFulfillment,
WaitingEscape,
Completed Completed
} }
public class SolverServerSession : EscrowReceiver public class SolverServerSession : EscrowReceiver
...@@ -289,7 +290,7 @@ namespace NTumbleBit.PuzzleSolver ...@@ -289,7 +290,7 @@ namespace NTumbleBit.PuzzleSolver
public TrustedBroadcastRequest GetSignedOfferTransaction() public TrustedBroadcastRequest GetSignedOfferTransaction()
{ {
AssertState(SolverServerStates.Completed); AssertState(SolverServerStates.WaitingEscape);
var offerTransaction = GetUnsignedOfferTransaction(); var offerTransaction = GetUnsignedOfferTransaction();
TransactionBuilder txBuilder = new TransactionBuilder(); TransactionBuilder txBuilder = new TransactionBuilder();
txBuilder.Extensions.Add(new EscrowBuilderExtension()); txBuilder.Extensions.Add(new EscrowBuilderExtension());
...@@ -307,7 +308,7 @@ namespace NTumbleBit.PuzzleSolver ...@@ -307,7 +308,7 @@ namespace NTumbleBit.PuzzleSolver
public SolutionKey[] GetSolutionKeys() public SolutionKey[] GetSolutionKeys()
{ {
AssertState(SolverServerStates.Completed); AssertState(SolverServerStates.WaitingEscape);
return InternalState.SolvedPuzzles.Select(s => s.SolutionKey).ToArray(); return InternalState.SolvedPuzzles.Select(s => s.SolutionKey).ToArray();
} }
...@@ -330,30 +331,36 @@ namespace NTumbleBit.PuzzleSolver ...@@ -330,30 +331,36 @@ namespace NTumbleBit.PuzzleSolver
Script offerScript = GetOfferScript(); Script offerScript = GetOfferScript();
var offer = GetUnsignedOfferTransaction(); var offer = GetUnsignedOfferTransaction();
PubKey clientKey = AssertValidSignature(clientSignature, offer);
TransactionBuilder builder = new TransactionBuilder(); TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.CheckFee = false;
builder.Extensions.Add(new EscrowBuilderExtension()); builder.Extensions.Add(new EscrowBuilderExtension());
builder.AddCoins(InternalState.EscrowedCoin); builder.AddCoins(InternalState.EscrowedCoin);
builder.AddKeys(InternalState.EscrowKey); builder.AddKeys(InternalState.EscrowKey);
var clientKey = InternalState.GetClientEscrowPubKey();
builder.AddKnownSignature(clientKey, clientSignature); builder.AddKnownSignature(clientKey, clientSignature);
builder.SignTransactionInPlace(offer); builder.SignTransactionInPlace(offer);
if(!builder.Verify(offer)) if(!builder.Verify(offer))
throw new PuzzleException("invalid-signature"); throw new PuzzleException("invalid-tumbler-signature");
var offerCoin = offer.Outputs.AsCoins().First().ToScriptCoin(offerScript); var offerCoin = offer.Outputs.AsCoins().First().ToScriptCoin(offerScript);
var solutions = InternalState.SolvedPuzzles.Select(s => s.SolutionKey).ToArray(); var solutions = InternalState.SolvedPuzzles.Select(s => s.SolutionKey).ToArray();
Transaction fulfill = new Transaction(); Transaction fulfill = new Transaction();
fulfill.Inputs.Add(new TxIn(offerCoin.Outpoint)); fulfill.Inputs.Add(new TxIn(offerCoin.Outpoint));
fulfill.Outputs.Add(new TxOut(offerCoin.Amount, cashout)); fulfill.Outputs.Add(new TxOut(offerCoin.Amount, cashout));
var size = new OfferBuilderExtension().EstimateScriptSigSize(offerCoin.Redeem);
fulfill.Outputs[0].Value -= feeRate.GetFee(size); var fulfillScript = SolverScriptBuilder.CreateFulfillScript(NBitcoin.BuilderExtensions.BuilderExtension.DummySignature, solutions);
fulfill.Inputs[0].ScriptSig = fulfillScript + Op.GetPushOp(offerCoin.Redeem.ToBytes());
fulfill.Outputs[0].Value -= feeRate.GetFee(fulfill.GetVirtualSize());
var signature = fulfill.Inputs.AsIndexedInputs().First().Sign(InternalState.FulfillKey, offerCoin, SigHash.All); var signature = fulfill.Inputs.AsIndexedInputs().First().Sign(InternalState.FulfillKey, offerCoin, SigHash.All);
var fulfillScript = SolverScriptBuilder.CreateFulfillScript(signature, solutions); fulfillScript = SolverScriptBuilder.CreateFulfillScript(signature, solutions);
fulfill.Inputs[0].ScriptSig = fulfillScript + Op.GetPushOp(offerCoin.Redeem.ToBytes()); fulfill.Inputs[0].ScriptSig = fulfillScript + Op.GetPushOp(offerCoin.Redeem.ToBytes());
InternalState.OfferClientSignature = clientSignature; InternalState.OfferClientSignature = clientSignature;
InternalState.Status = SolverServerStates.Completed; InternalState.Status = SolverServerStates.WaitingEscape;
return new TrustedBroadcastRequest return new TrustedBroadcastRequest
{ {
Key = InternalState.FulfillKey, Key = InternalState.FulfillKey,
...@@ -362,6 +369,40 @@ namespace NTumbleBit.PuzzleSolver ...@@ -362,6 +369,40 @@ namespace NTumbleBit.PuzzleSolver
}; };
} }
private PubKey AssertValidSignature(TransactionSignature clientSignature, Transaction offer)
{
var signedHash = offer.Inputs.AsIndexedInputs().First().GetSignatureHash(InternalState.EscrowedCoin, clientSignature.SigHash);
var clientKey = InternalState.GetClientEscrowPubKey();
if(!clientKey.Verify(signedHash, clientSignature.Signature))
throw new PuzzleException("invalid-client-signature");
return clientKey;
}
public Transaction GetSignedEscapeTransaction(TransactionSignature clientSignature, Script cashout)
{
AssertState(SolverServerStates.WaitingEscape);
var escapeTx = GetUnsignedOfferTransaction();
escapeTx.Outputs[0].ScriptPubKey = cashout;
var clientKey = AssertValidSignature(clientSignature, escapeTx);
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.CheckFee = false;
builder.Extensions.Add(new EscrowBuilderExtension());
builder.AddCoins(InternalState.EscrowedCoin);
builder.AddKnownSignature(clientKey, clientSignature);
//This add the known signature if correct SigHash
builder.SignTransactionInPlace(escapeTx, SigHash.None | SigHash.AnyoneCanPay);
//This sign SigHash.All
builder.AddKeys(InternalState.EscrowKey);
builder.SignTransactionInPlace(escapeTx);
if(!builder.Verify(escapeTx))
throw new PuzzleException("invalid-tumbler-signature");
return escapeTx;
}
private Script GetOfferScript() private Script GetOfferScript()
{ {
var escrow = EscrowScriptBuilder.ExtractEscrowScriptPubKeyParameters(InternalState.EscrowedCoin.Redeem); var escrow = EscrowScriptBuilder.ExtractEscrowScriptPubKeyParameters(InternalState.EscrowedCoin.Redeem);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment