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
try
{
await this.tumbleBitManager.TumbleAsync(request.DestinationWalletName);
await this.tumbleBitManager.TumbleAsync(request.OriginWalletName, request.DestinationWalletName);
return this.Ok();
}
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 @@
void Save();
/// <summary>
/// Loads the state of the current tumbling session.
/// Loads the saved state of the tumbling execution to the file system.
/// </summary>
/// <returns></returns>
IStateMachine Load();
void LoadStateFromMemory();
/// <summary>
/// Deletes the state of the current tumbling session..
......
......@@ -17,7 +17,7 @@ namespace Breeze.TumbleBit.Client
/// <returns></returns>
Task<ClassicTumblerParameters> ConnectToTumblerAsync(Uri serverAddress);
Task TumbleAsync(string destinationWalletName);
Task TumbleAsync(string originWalletName, string destinationWalletName);
/// <summary>
/// Processes a block received from the network.
......
......@@ -40,5 +40,7 @@ namespace Breeze.TumbleBit.Client
Task<PuzzleSolver.ServerCommitment[]> SolvePuzzlesAsync(int cycleId, string channelId, PuzzleValue[] puzzles);
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
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; }
}
}
using NTumbleBit.ClassicTumbler;
using NTumbleBit.PuzzleSolver;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NTumbleBit.PuzzlePromise;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NBitcoin.SPV;
using NTumbleBit;
using Stratis.Bitcoin.Wallet;
using Wallet = Stratis.Bitcoin.Wallet.Wallet;
namespace Breeze.TumbleBit.Client
{
public partial class TumblingState : IStateMachine
{
public bool NonCooperative { get; set; }
private ExternalServices services = new ExternalServices();
public async Task Update(Session session)
{
int height = this.chain.Tip.Height;
CycleParameters cycle;
CyclePhase phase;
if (session.ClientChannelNegotiation == null)
{
cycle = this.TumblerParameters.CycleGenerator.GetRegistratingCycle(height);
phase = CyclePhase.Registration;
}
else
{
cycle = session.ClientChannelNegotiation.GetCycle();
var phases = new CyclePhase[]
{
CyclePhase.Registration,
CyclePhase.ClientChannelEstablishment,
CyclePhase.TumblerChannelEstablishment,
CyclePhase.PaymentPhase,
CyclePhase.TumblerCashoutPhase,
CyclePhase.ClientCashoutPhase
};
if (!phases.Any(p => cycle.IsInPhase(p, height)))
return;
phase = phases.First(p => cycle.IsInPhase(p, height));
}
logger.LogInformation("Cycle " + cycle.Start + " in phase " + Enum.GetName(typeof(CyclePhase), phase) + ", ending in " + (cycle.GetPeriods().GetPeriod(phase).End - height) + " blocks");
var correlation = session.SolverClientSession == null ? 0 : GetCorrelation(session.SolverClientSession.EscrowedCoin.ScriptPubKey);
FeeRate feeRate = null;
switch (phase)
{
case CyclePhase.Registration:
if (session.ClientChannelNegotiation == null)
{
//Client asks for voucher
var voucherResponse = await this.BobClient.AskUnsignedVoucherAsync();
//Client ensures he is in the same cycle as the tumbler (would fail if one tumbler or client's chain isn't sync)
var tumblerCycle = this.TumblerParameters.CycleGenerator.GetCycle(voucherResponse.CycleStart);
Assert(tumblerCycle.Start == cycle.Start, "invalid-phase");
//Saving the voucher for later
session.StartCycle = cycle.Start;
session.ClientChannelNegotiation = new ClientChannelNegotiation(this.TumblerParameters, cycle.Start);
session.ClientChannelNegotiation.ReceiveUnsignedVoucher(voucherResponse);
logger.LogInformation("Registration Complete");
}
break;
case CyclePhase.ClientChannelEstablishment:
if (session.ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingTumblerClientTransactionKey)
{
var key = await this.AliceClient.RequestTumblerEscrowKeyAsync(cycle.Start);
session.ClientChannelNegotiation.ReceiveTumblerEscrowKey(key.PubKey, key.KeyIndex);
//Client create the escrow
var escrowTxOut = session.ClientChannelNegotiation.BuildClientEscrowTxOut();
feeRate = GetFeeRate();
Transaction clientEscrowTx = null;
try
{
clientEscrowTx = services.FundTransaction(escrowTxOut, feeRate);
}
catch (NotEnoughFundsException ex)
{
logger.LogInformation($"Not enough funds in the wallet to tumble. Missing about {ex.Missing}. Denomination is {this.TumblerParameters.Denomination}.");
break;
}
session.SolverClientSession = session.ClientChannelNegotiation.SetClientSignedTransaction(clientEscrowTx);
correlation = GetCorrelation(session.SolverClientSession.EscrowedCoin.ScriptPubKey);
// Tracker.AddressCreated(cycle.Start, TransactionType.ClientEscrow, escrowTxOut.ScriptPubKey, correlation);
// Tracker.TransactionCreated(cycle.Start, TransactionType.ClientEscrow, clientEscrowTx.GetHash(), correlation);
services.Track(escrowTxOut.ScriptPubKey);
var redeemDestination = this.OriginWallet.GetAccountsByCoinType(this.coinType).First().GetFirstUnusedReceivingAddress().ScriptPubKey;// Services.WalletService.GenerateAddress().ScriptPubKey;
var redeemTx = session.SolverClientSession.CreateRedeemTransaction(feeRate, redeemDestination);
//Tracker.AddressCreated(cycle.Start, TransactionType.ClientRedeem, redeemDestination, correlation);
//redeemTx does not be to be recorded to the tracker, this is TrustedBroadcastService job
services.Broadcast(clientEscrowTx);
services.TrustedBroadcast(cycle.Start, TransactionType.ClientRedeem, correlation, redeemTx);
logger.LogInformation("Client escrow broadcasted " + clientEscrowTx.GetHash());
logger.LogInformation("Client escrow redeem " + redeemTx.Transaction.GetHash() + " will be broadcast later if tumbler unresponsive");
}
else if (session.ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingSolvedVoucher)
{
TransactionInformation clientTx = GetTransactionInformation(session.SolverClientSession.EscrowedCoin, true);
var state = session.ClientChannelNegotiation.GetInternalState();
if (clientTx != null && clientTx.Confirmations >= cycle.SafetyPeriodDuration)
{
//Client asks the public key of the Tumbler and sends its own
var aliceEscrowInformation = session.ClientChannelNegotiation.GenerateClientTransactionKeys();
var voucher = await this.AliceClient.SignVoucherAsync(new Models.SignVoucherRequest
{
MerkleProof = clientTx.MerkleProof,
Transaction = clientTx.Transaction,
KeyReference = state.TumblerEscrowKeyReference,
ClientEscrowInformation = aliceEscrowInformation,
TumblerEscrowPubKey = state.ClientEscrowInformation.OtherEscrowKey
});
session.ClientChannelNegotiation.CheckVoucherSolution(voucher);
logger.LogInformation("Voucher solution obtained");
}
}
break;
case CyclePhase.TumblerChannelEstablishment:
if (session.ClientChannelNegotiation != null && session.ClientChannelNegotiation.Status == TumblerClientSessionStates.WaitingGenerateTumblerTransactionKey)
{
//Client asks the Tumbler to make a channel
var bobEscrowInformation = session.ClientChannelNegotiation.GetOpenChannelRequest();
var tumblerInformation = await this.BobClient.OpenChannelAsync(bobEscrowInformation);
session.PromiseClientSession = session.ClientChannelNegotiation.ReceiveTumblerEscrowedCoin(tumblerInformation);
//Tell to the block explorer we need to track that address (for checking if it is confirmed in payment phase)
services.Track(session.PromiseClientSession.EscrowedCoin.ScriptPubKey);
//Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.ScriptPubKey, correlation);
//Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, PromiseClientSession.EscrowedCoin.Outpoint.Hash, correlation);
//Channel is done, now need to run the promise protocol to get valid puzzle
var cashoutDestination = this.DestinationWallet.GetAccountsByCoinType(CoinType.Bitcoin).First().GetFirstUnusedReceivingAddress().ScriptPubKey;
//Tracker.AddressCreated(cycle.Start, TransactionType.TumblerCashout, cashoutDestination, correlation);
feeRate = GetFeeRate();
var sigReq = session.PromiseClientSession.CreateSignatureRequest(cashoutDestination, feeRate);
var commiments = await this.BobClient.SignHashesAsync(cycle.Start, session.PromiseClientSession.Id, sigReq);
var revelation = session.PromiseClientSession.Reveal(commiments);
var proof = await this.BobClient.CheckRevelationAsync(cycle.Start, session.PromiseClientSession.Id, revelation);
var puzzle = session.PromiseClientSession.CheckCommitmentProof(proof);
session.SolverClientSession.AcceptPuzzle(puzzle);
logger.LogInformation("Tumbler escrow broadcasted " + session.PromiseClientSession.EscrowedCoin.Outpoint.Hash);
}
break;
case CyclePhase.PaymentPhase:
if (session.PromiseClientSession != null)
{
TransactionInformation tumblerTx = GetTransactionInformation(session.PromiseClientSession.EscrowedCoin, false);
//Ensure the tumbler coin is confirmed before paying anything
if (tumblerTx == null || tumblerTx.Confirmations < cycle.SafetyPeriodDuration)
{
if (tumblerTx != null)
logger.LogInformation("Tumbler escrow " + tumblerTx.Transaction.GetHash() + " expecting " + cycle.SafetyPeriodDuration + " current is " + tumblerTx.Confirmations);
else
logger.LogInformation("Tumbler escrow not found");
return;
}
if (session.SolverClientSession.Status == SolverClientStates.WaitingGeneratePuzzles)
{
logger.LogInformation("Tumbler escrow confirmed " + tumblerTx.Transaction.GetHash());
feeRate = GetFeeRate();
var puzzles = session.SolverClientSession.GeneratePuzzles();
var commmitments = await this.AliceClient.SolvePuzzlesAsync(cycle.Start, session.SolverClientSession.Id, puzzles);
var revelation2 = session.SolverClientSession.Reveal(commmitments);
var solutionKeys = await this.AliceClient.CheckRevelationAsync(cycle.Start, session.SolverClientSession.Id, revelation2);
var blindFactors = session.SolverClientSession.GetBlindFactors(solutionKeys);
var offerInformation = await this.AliceClient.CheckBlindFactorsAsync(cycle.Start, session.SolverClientSession.Id, blindFactors);
var offerSignature = session.SolverClientSession.SignOffer(offerInformation);
var offerRedeemAddress = this.OriginWallet.GetAccountsByCoinType(this.coinType).First().GetFirstUnusedReceivingAddress(); // Services.WalletService.GenerateAddress($"Cycle {cycle.Start} Tumbler Redeem").ScriptPubKey);
var offerRedeem = session.SolverClientSession.CreateOfferRedeemTransaction(feeRate, offerRedeemAddress.ScriptPubKey);
//May need to find solution in the fulfillment transaction
services.Track(offerRedeem.PreviousScriptPubKey);
//Tracker.AddressCreated(cycle.Start, TransactionType.ClientOfferRedeem, offerRedeemAddress.ScriptPubKey, correlation);
services.TrustedBroadcast(cycle.Start, TransactionType.ClientOfferRedeem, correlation, offerRedeem);
logger.LogInformation("Offer redeem " + offerRedeem.Transaction.GetHash() + " locked until " + offerRedeem.Transaction.LockTime.Height);
try
{
solutionKeys = await this.AliceClient.FulfillOfferAsync(cycle.Start, session.SolverClientSession.Id, offerSignature);
session.SolverClientSession.CheckSolutions(solutionKeys);
var tumblingSolution = session.SolverClientSession.GetSolution();
var transaction = session.PromiseClientSession.GetSignedTransaction(tumblingSolution);
services.TrustedBroadcast(cycle.Start, TransactionType.TumblerCashout, correlation, new TrustedBroadcastRequest()
{
BroadcastAt = cycle.GetPeriods().ClientCashout.Start,
Transaction = transaction
});
if (!NonCooperative)
{
var signature = session.SolverClientSession.SignEscape();
await this.AliceClient.GiveEscapeKeyAsync(cycle.Start, session.SolverClientSession.Id, signature);
}
logger.LogInformation("Solution recovered from cooperative tumbler");
}
catch (Exception ex)
{
logger.LogWarning("Uncooperative tumbler detected, keep connection open.");
logger.LogWarning(ex.ToString());
}
logger.LogInformation("Payment completed");
}
}
break;
case CyclePhase.ClientCashoutPhase:
if (session.SolverClientSession != null)
{
//If the tumbler is uncooperative, he published solutions on the blockchain
if (session.SolverClientSession.Status == SolverClientStates.WaitingPuzzleSolutions)
{
var transactions = services.GetTransactions(session.SolverClientSession.GetOfferScriptPubKey(), false);
if (transactions.Length == 0)
{
logger.LogInformation("Solution of puzzle not on the blockchain");
}
else
{
session.SolverClientSession.CheckSolutions(transactions.Select(t => t.Transaction).ToArray());
logger.LogInformation("Solution recovered from blockchain transaction");
var tumblingSolution = session.SolverClientSession.GetSolution();
var transaction = session.PromiseClientSession.GetSignedTransaction(tumblingSolution);
// Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerCashout, transaction.GetHash(), correlation);
services.Broadcast(transaction);
logger.LogInformation("Client Cashout completed " + transaction.GetHash());
}
}
}
break;
}
}
private uint GetCorrelation(Script scriptPubKey)
{
return new uint160(scriptPubKey.Hash.ToString()).GetLow32();
}
private TransactionInformation GetTransactionInformation(ICoin coin, bool withProof)
{
var tx = services.GetTransactions(coin.TxOut.ScriptPubKey, withProof)
.FirstOrDefault(t => t.Transaction.GetHash() == coin.Outpoint.Hash);
return tx;
}
private FeeRate GetFeeRate()
{
return services.GetFeeRate();
}
private void Assert(bool test, string error)
{
if (!test)
throw new PuzzleException(error);
}
}
public class TransactionInformation
{
public int Confirmations
{
get; set;
}
public MerkleBlock MerkleProof
{
get;
set;
}
public Transaction Transaction
{
get; set;
}
}
}
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
private readonly Network network;
private TumblingState tumblingState;
private IDisposable blockReceiver;
int lastCycleStarted;
private ClassicTumblerParameters TumblerParameters { get; set; }
public TumbleBitManager(ILoggerFactory loggerFactory, IWalletManager walletManager, ConcurrentChain chain, Network network, Signals signals)
{
this.lastCycleStarted = 0;
this.walletManager = walletManager;
this.chain = chain;
this.signals = signals;
this.network = network;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
// load the persisted tumbling state
this.tumblingState = TumblingState.LoadState();
this.tumblingState = new TumblingState(loggerFactory, this.chain, this.walletManager, this.network);
}
/// <inheritdoc />
......@@ -51,20 +48,20 @@ namespace Breeze.TumbleBit.Client
throw new Exception($"The tumbler is on network {this.TumblerParameters.Network} while the wallet is on network {this.network}.");
}
if (this.tumblingState == null)
{
this.tumblingState = new TumblingState();
}
// load the current tumbling state fromt he file system
this.tumblingState.LoadStateFromMemory();
// update and save the state
this.tumblingState.TumblerUri = serverAddress;
this.tumblingState.TumblerParameters = this.TumblerParameters;
this.tumblingState.SetClients(this.tumblerService);
this.tumblingState.Save();
return this.TumblerParameters;
}
/// <inheritdoc />
public Task TumbleAsync(string destinationWalletName)
public Task TumbleAsync(string originWalletName, string destinationWalletName)
{
// make sure the tumbler service is initialized
if (this.TumblerParameters == null || this.tumblerService == null)
......@@ -81,11 +78,21 @@ namespace Breeze.TumbleBit.Client
Wallet destinationWallet = this.walletManager.GetWallet(destinationWalletName);
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
this.tumblingState.DestinationWallet = destinationWallet;
this.tumblingState.DestinationWalletName = destinationWalletName;
this.tumblingState.OriginWallet = originWallet;
this.tumblingState.OriginWalletName = originWalletName;
this.tumblingState.Save();
// subscribe to receiving blocks
......@@ -114,29 +121,12 @@ namespace Breeze.TumbleBit.Client
/// <inheritdoc />
public void ProcessBlock(int height, Block block)
{
// TODO start the state machine
this.logger.LogDebug($"Receive block with height {height}");
this.logger.LogDebug($"Received block with height {height} during tumbling session.");
// update the block height in the tumbling state
this.tumblingState.LastBlockReceivedHeight = height;
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
this.tumblingState.Update();
}
......
......@@ -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[]>();
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;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Newtonsoft.Json;
using NTumbleBit.ClassicTumbler;
using NTumbleBit.PuzzlePromise;
using NTumbleBit.PuzzleSolver;
using Stratis.Bitcoin.Wallet;
namespace Breeze.TumbleBit.Client
{
public class TumblingState : IStateMachine
public partial class TumblingState : IStateMachine
{
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")]
public ClassicTumblerParameters TumblerParameters { get; set; }
[JsonProperty("tumblerUri")]
public Uri TumblerUri { get; set; }
[JsonProperty("lastBlockReceivedHeight")]
[JsonProperty("lastBlockReceivedHeight", DefaultValueHandling = DefaultValueHandling.Ignore)]
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; }
[JsonProperty("sessions", NullValueHandling = NullValueHandling.Ignore)]
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()
{
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 />
......@@ -41,9 +81,23 @@ namespace Breeze.TumbleBit.Client
}
/// <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 />
......@@ -56,31 +110,51 @@ namespace Breeze.TumbleBit.Client
/// <inheritdoc />
public void Update()
{
// get a list of cycles we expect to have at this height
var cycles = this.TumblerParameters.CycleGenerator.GetCycles(this.LastBlockReceivedHeight);
// get the next cycle to be started
var cycle = this.TumblerParameters.CycleGenerator.GetRegistratingCycle(this.LastBlockReceivedHeight);
var states = cycles.SelectMany(c => this.Sessions.Where(s => s.StartCycle == c.Start)).ToList();
foreach (var state in states)
// create a new session if allowed
if (this.Sessions.Count == 0)
{
try
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
var cycles = this.TumblerParameters.CycleGenerator.GetCycles(this.LastBlockReceivedHeight);
var existingSessions = cycles.SelectMany(c => this.Sessions.Where(s => s.StartCycle == c.Start)).ToList();
foreach (var existingSession in existingSessions)
{
// create a new session to be updated
var session = new Session();
if (state.NegotiationClientState != null)
if (existingSession.NegotiationClientState != null)
{
session.StartCycle = state.NegotiationClientState.CycleStart;
session.ClientChannelNegotiation = new ClientChannelNegotiation(this.TumblerParameters, state.NegotiationClientState);
session.StartCycle = existingSession.NegotiationClientState.CycleStart;
session.ClientChannelNegotiation = new ClientChannelNegotiation(this.TumblerParameters, existingSession.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);
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
session.Update();
this.MoveToNextPhase(session);
// replace the updated session in the list of existing sessions
int index = this.Sessions.IndexOf(state);
int index = this.Sessions.IndexOf(existingSession);
if (index != -1)
{
this.Sessions[index] = session;
......@@ -88,12 +162,11 @@ namespace Breeze.TumbleBit.Client
this.Save();
}
catch (Exception)
{
throw;
}
}
public void MoveToNextPhase(Session session)
{
this.logger.LogInformation($"Entering next phase for cycle {session.StartCycle}.");
}
public void CreateNewSession(int start)
......@@ -102,22 +175,6 @@ namespace Breeze.TumbleBit.Client
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>
/// Gets the file path of the file containing the state of the tumbling execution.
/// </summary>
......@@ -152,15 +209,13 @@ namespace Breeze.TumbleBit.Client
public SolverClientSession.State SolverClientState { get; set; }
[JsonIgnore]
public ClientChannelNegotiation ClientChannelNegotiation { get; set; }
[JsonIgnore]
public SolverClientSession SolverClientSession { get; set; }
[JsonIgnore]
public PromiseClientSession PromiseClientSession { get; set; }
public void Update()
{
}
}
}
......@@ -71,8 +71,9 @@ namespace NTumbleBit
tx.Inputs.Add(new TxIn(coin.Outpoint));
tx.Inputs[0].Sequence = 0;
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());
var vSize = tx.GetVirtualSize() + 80; // Size without signature + the signature size
tx.Outputs[0].Value -= feeRate.GetFee(vSize);
var redeemTransaction = new TrustedBroadcastRequest
......
using NBitcoin.RPC;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
......@@ -16,5 +17,33 @@ namespace NTumbleBit
Params = parameters
}, 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
Transaction cashout = new Transaction();
cashout.AddInput(new TxIn(InternalState.EscrowedCoin.Outpoint, Script.Empty));
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;
......
......@@ -318,6 +318,15 @@ namespace NTumbleBit.PuzzleSolver
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()
{
Script offer = CreateOfferScript();
......@@ -333,15 +342,17 @@ namespace NTumbleBit.PuzzleSolver
{
var coin = CreateUnsignedOfferTransaction().Outputs.AsCoins().First().ToScriptCoin(CreateOfferScript());
var unknownOutpoints = new OutPoint(uint256.Zero, 0);
Transaction tx = new Transaction();
tx.LockTime = CreateOfferScriptParameters().Expiration;
tx.Inputs.Add(new TxIn(coin.Outpoint));
tx.Inputs.Add(new TxIn(unknownOutpoints));
tx.Inputs[0].Sequence = 0;
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());
var vSize = tx.GetVirtualSize() + 80; // Size without signature + the signature size
tx.Outputs[0].Value -= feeRate.GetFee(vSize);
var redeemTransaction = new TrustedBroadcastRequest
{
Key = InternalState.RedeemKey,
......@@ -349,7 +360,7 @@ namespace NTumbleBit.PuzzleSolver
Transaction = tx
};
//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;
}
......
......@@ -20,6 +20,7 @@ namespace NTumbleBit.PuzzleSolver
WaitingRevelation,
WaitingBlindFactor,
WaitingFulfillment,
WaitingEscape,
Completed
}
public class SolverServerSession : EscrowReceiver
......@@ -289,7 +290,7 @@ namespace NTumbleBit.PuzzleSolver
public TrustedBroadcastRequest GetSignedOfferTransaction()
{
AssertState(SolverServerStates.Completed);
AssertState(SolverServerStates.WaitingEscape);
var offerTransaction = GetUnsignedOfferTransaction();
TransactionBuilder txBuilder = new TransactionBuilder();
txBuilder.Extensions.Add(new EscrowBuilderExtension());
......@@ -307,7 +308,7 @@ namespace NTumbleBit.PuzzleSolver
public SolutionKey[] GetSolutionKeys()
{
AssertState(SolverServerStates.Completed);
AssertState(SolverServerStates.WaitingEscape);
return InternalState.SolvedPuzzles.Select(s => s.SolutionKey).ToArray();
}
......@@ -330,30 +331,36 @@ namespace NTumbleBit.PuzzleSolver
Script offerScript = GetOfferScript();
var offer = GetUnsignedOfferTransaction();
PubKey clientKey = AssertValidSignature(clientSignature, offer);
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.CheckFee = false;
builder.Extensions.Add(new EscrowBuilderExtension());
builder.AddCoins(InternalState.EscrowedCoin);
builder.AddKeys(InternalState.EscrowKey);
var clientKey = InternalState.GetClientEscrowPubKey();
builder.AddKnownSignature(clientKey, clientSignature);
builder.SignTransactionInPlace(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 solutions = InternalState.SolvedPuzzles.Select(s => s.SolutionKey).ToArray();
Transaction fulfill = new Transaction();
fulfill.Inputs.Add(new TxIn(offerCoin.Outpoint));
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 fulfillScript = SolverScriptBuilder.CreateFulfillScript(signature, solutions);
fulfillScript = SolverScriptBuilder.CreateFulfillScript(signature, solutions);
fulfill.Inputs[0].ScriptSig = fulfillScript + Op.GetPushOp(offerCoin.Redeem.ToBytes());
InternalState.OfferClientSignature = clientSignature;
InternalState.Status = SolverServerStates.Completed;
InternalState.Status = SolverServerStates.WaitingEscape;
return new TrustedBroadcastRequest
{
Key = InternalState.FulfillKey,
......@@ -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()
{
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