public async Task SuccessFromPreviousCoinJoinAsync() { WabiSabiConfig cfg = new(); var round = WabiSabiFactory.CreateRound(cfg); using Key key = new(); var coin = WabiSabiFactory.CreateCoin(key); var rpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin); var coinJoinIdsStore = new InMemoryCoinJoinIdStore(); coinJoinIdsStore.Add(coin.Outpoint.Hash); using Arena arena = await ArenaBuilder.From(cfg).With(rpc).With(coinJoinIdsStore).CreateAndStartAsync(round); var minAliceDeadline = DateTimeOffset.UtcNow + cfg.ConnectionConfirmationTimeout * 0.9; var arenaClient = WabiSabiFactory.CreateArenaClient(arena); var ownershipProof = WabiSabiFactory.CreateOwnershipProof(key, round.Id); var(resp, _) = await arenaClient.RegisterInputAsync(round.Id, coin.Outpoint, ownershipProof, CancellationToken.None); AssertSingleAliceSuccessfullyRegistered(round, minAliceDeadline, resp); var myAlice = Assert.Single(round.Alices); Assert.True(myAlice.IsPayingZeroCoordinationFee); await arena.StopAsync(CancellationToken.None); }
public WabiSabiCoordinator(CoordinatorParameters parameters, IRPCClient rpc) { Parameters = parameters; Warden = new(parameters.UtxoWardenPeriod, parameters.PrisonFilePath, Config); ConfigWatcher = new(parameters.ConfigChangeMonitoringPeriod, Config, () => Logger.LogInfo("WabiSabi configuration has changed.")); CoinJoinTransactionArchiver transactionArchiver = new(Path.Combine(parameters.CoordinatorDataDir, "CoinJoinTransactions")); CoinJoinFeeRateStatStore = CoinJoinFeeRateStatStore.LoadFromFile(parameters.CoinJoinFeeRateStatStoreFilePath, Config, rpc); IoHelpers.EnsureContainingDirectoryExists(Parameters.CoinJoinFeeRateStatStoreFilePath); CoinJoinFeeRateStatStore.NewStat += FeeRateStatStore_NewStat; var inMemoryCoinJoinIdStore = InMemoryCoinJoinIdStore.LoadFromFile(parameters.CoinJoinIdStoreFilePath); var coinJoinScriptStore = CoinJoinScriptStore.LoadFromFile(parameters.CoinJoinScriptStoreFilePath); IoHelpers.EnsureContainingDirectoryExists(Parameters.CoinJoinScriptStoreFilePath); Arena = new( parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc, Warden.Prison, inMemoryCoinJoinIdStore, transactionArchiver, coinJoinScriptStore); IoHelpers.EnsureContainingDirectoryExists(Parameters.CoinJoinIdStoreFilePath); Arena.CoinJoinBroadcast += Arena_CoinJoinBroadcast; }
public static InMemoryCoinJoinIdStore LoadFromFile(string filePath) { var lines = File.Exists(filePath) ? File.ReadAllLines(filePath).Select(x => uint256.Parse(x)) : Enumerable.Empty <uint256>(); var store = new InMemoryCoinJoinIdStore(lines); return(store); }
public Arena( TimeSpan period, Network network, WabiSabiConfig config, IRPCClient rpc, Prison prison, InMemoryCoinJoinIdStore inMemoryCoinJoinIdStore, CoinJoinTransactionArchiver?archiver = null) : base(period) { Network = network; Config = config; Rpc = rpc; Prison = prison; TransactionArchiver = archiver; Random = new SecureRandom(); InMemoryCoinJoinIdStore = inMemoryCoinJoinIdStore; }
private async Task <InputRegistrationResponse> RegisterInputCoreAsync(InputRegistrationRequest request, CancellationToken cancellationToken) { var coin = await OutpointToCoinAsync(request, cancellationToken).ConfigureAwait(false); using (await AsyncLock.LockAsync(cancellationToken).ConfigureAwait(false)) { var round = GetRound(request.RoundId); var registeredCoins = Rounds.Where(x => !(x.Phase == Phase.Ended && !x.WasTransactionBroadcast)) .SelectMany(r => r.Alices.Select(a => a.Coin)); if (registeredCoins.Any(x => x.Outpoint == coin.Outpoint)) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.AliceAlreadyRegistered); } if (round.IsInputRegistrationEnded(Config.MaxInputCountByRound)) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongPhase); } if (round is BlameRound blameRound && !blameRound.BlameWhitelist.Contains(coin.Outpoint)) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.InputNotWhitelisted); } // Compute but don't commit updated coinjoin to round state, it will // be re-calculated on input confirmation. This is computed in here // for validation purposes. _ = round.Assert <ConstructionState>().AddInput(coin); var coinJoinInputCommitmentData = new CoinJoinInputCommitmentData("CoinJoinCoordinatorIdentifier", round.Id); if (!OwnershipProof.VerifyCoinJoinInputProof(request.OwnershipProof, coin.TxOut.ScriptPubKey, coinJoinInputCommitmentData)) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.WrongOwnershipProof); } // Generate a new GUID with the secure random source, to be sure // that it is not guessable (Guid.NewGuid() documentation does // not say anything about GUID version or randomness source, // only that the probability of duplicates is very low). var id = new Guid(Random.GetBytes(16)); var isPayingZeroCoordinationFee = InMemoryCoinJoinIdStore.Contains(coin.Outpoint.Hash); if (!isPayingZeroCoordinationFee) { // If the coin comes from a tx that all of the tx inputs are coming from a CJ (1 hop - no pay). Transaction tx = await Rpc.GetRawTransactionAsync(coin.Outpoint.Hash, true, cancellationToken).ConfigureAwait(false); if (tx.Inputs.All(input => InMemoryCoinJoinIdStore.Contains(input.PrevOut.Hash))) { isPayingZeroCoordinationFee = true; } } var alice = new Alice(coin, request.OwnershipProof, round, id, isPayingZeroCoordinationFee); if (alice.CalculateRemainingAmountCredentials(round.FeeRate, round.CoordinationFeeRate) <= Money.Zero) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.UneconomicalInput); } if (alice.TotalInputAmount < round.MinAmountCredentialValue) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds); } if (alice.TotalInputAmount > round.MaxAmountCredentialValue) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds); } if (alice.TotalInputVsize > round.MaxVsizeAllocationPerAlice) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchVsize); } var amountCredentialTask = round.AmountCredentialIssuer.HandleRequestAsync(request.ZeroAmountCredentialRequests, cancellationToken); var vsizeCredentialTask = round.VsizeCredentialIssuer.HandleRequestAsync(request.ZeroVsizeCredentialRequests, cancellationToken); if (round.RemainingInputVsizeAllocation < round.MaxVsizeAllocationPerAlice) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.VsizeQuotaExceeded); } var commitAmountCredentialResponse = await amountCredentialTask.ConfigureAwait(false); var commitVsizeCredentialResponse = await vsizeCredentialTask.ConfigureAwait(false); alice.SetDeadlineRelativeTo(round.ConnectionConfirmationTimeFrame.Duration); round.Alices.Add(alice); return(new(alice.Id, commitAmountCredentialResponse, commitVsizeCredentialResponse, alice.IsPayingZeroCoordinationFee)); } }