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 coinJoinIdStore = new CoinJoinIdStore(); coinJoinIdStore.TryAdd(coin.Outpoint.Hash); using Arena arena = await ArenaBuilder.From(cfg).With(rpc).With(coinJoinIdStore).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 void CanAdd() { var cjIdStore = new CoinJoinIdStore(); cjIdStore.TryAdd(uint256.One); Assert.True(cjIdStore.Contains(uint256.One)); cjIdStore.TryAdd(uint256.One); cjIdStore.TryAdd(uint256.One); Assert.Single(cjIdStore.GetCoinJoinIds); }
public void CanTolerateError() { var listOfCoinjoinHashes = new List <string> { "9690826aab03c7b9ca15af2d79083445df1ac94e79858acc146efa9a52c73b5b", "dummy", "90f1e3893a890ae314fba50c3dc870b0b5e5aab6e14f9e0fe9e56c95f20a2b36", "1fa685dbf8273369762a4f88ad1ce7f3fd14907130b878fbbb96e4140bf2bc96" }; IEnumerable <uint256> ids = CoinJoinIdStore.GetValidCoinjoinIds(listOfCoinjoinHashes, out var validCoinjoinIds, out bool wasError); Assert.Equal(ids.Count(), validCoinjoinIds.Count); Assert.Equal(ids.Count(), listOfCoinjoinHashes.Count - 1); Assert.True(wasError); }
public void CanValidate() { var listOfCoinjoinHashes = new List <string> { "9690826aab03c7b9ca15af2d79083445df1ac94e79858acc146efa9a52c73b5b", "a79e7544d32f9c5e0c3a6ed9bcbb29723125f38461bb7a735823eddc7dac7ad2", "90f1e3893a890ae314fba50c3dc870b0b5e5aab6e14f9e0fe9e56c95f20a2b36", "1fa685dbf8273369762a4f88ad1ce7f3fd14907130b878fbbb96e4140bf2bc96" }; IEnumerable <uint256> ids = CoinJoinIdStore.GetValidCoinjoinIds(listOfCoinjoinHashes, out var validCoinjoinIds, out bool wasError); Assert.Equal(listOfCoinjoinHashes.Count, ids.Count()); Assert.Equal(listOfCoinjoinHashes, validCoinjoinIds); Assert.False(wasError); }
private void Arena_CoinJoinBroadcast(object?sender, Transaction transaction) { LastSuccessfulCoinJoinTime = DateTimeOffset.UtcNow; CoinJoinIdStore.TryAdd(transaction.GetHash()); var coinJoinScriptStoreFilePath = Parameters.CoinJoinScriptStoreFilePath; try { File.AppendAllLines(coinJoinScriptStoreFilePath, transaction.Outputs.Select(x => x.ScriptPubKey.ToHex())); } catch (Exception ex) { Logger.LogError($"Could not write file {coinJoinScriptStoreFilePath}.", ex); } }
public async Task InitializeAsync(Config config, CoordinatorRoundConfig roundConfig, IRPCClient rpc, CancellationToken cancel) { Config = Guard.NotNull(nameof(config), config); RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig); RpcClient = Guard.NotNull(nameof(rpc), rpc); // Make sure RPC works. await AssertRpcNodeFullyInitializedAsync(cancel); // Make sure P2P works. await InitializeP2pAsync(config.Network, config.GetBitcoinP2pEndPoint(), cancel); var p2pNode = Guard.NotNull(nameof(P2pNode), P2pNode); HostedServices.Register <MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(21), RpcClient, p2pNode), "Full Node Mempool Mirror"); // Initialize index building var indexBuilderServiceDir = Path.Combine(DataDir, "IndexBuilderService"); var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{RpcClient.Network}.dat"); var blockNotifier = HostedServices.Get <BlockNotifier>(); CoordinatorParameters coordinatorParameters = new(DataDir); Coordinator = new(RpcClient.Network, blockNotifier, Path.Combine(DataDir, "CcjCoordinator"), RpcClient, roundConfig); Coordinator.CoinJoinBroadcasted += Coordinator_CoinJoinBroadcasted; var coordinator = Guard.NotNull(nameof(Coordinator), Coordinator); if (!string.IsNullOrWhiteSpace(roundConfig.FilePath)) { HostedServices.Register <ConfigWatcher>(() => new ConfigWatcher( TimeSpan.FromSeconds(10), // Every 10 seconds check the config RoundConfig, () => { try { coordinator.RoundConfig.UpdateOrDefault(RoundConfig, toFile: false); coordinator.AbortAllRoundsInInputRegistration($"{nameof(RoundConfig)} has changed."); } catch (Exception ex) { Logger.LogDebug(ex); } }), "Config Watcher"); } CoinJoinIdStore = CoinJoinIdStore.Create(Coordinator.CoinJoinsFilePath, coordinatorParameters.CoinJoinIdStoreFilePath); var coinJoinScriptStore = CoinJoinScriptStore.LoadFromFile(coordinatorParameters.CoinJoinScriptStoreFilePath); WabiSabiCoordinator = new WabiSabiCoordinator(coordinatorParameters, RpcClient, CoinJoinIdStore, coinJoinScriptStore); HostedServices.Register <WabiSabiCoordinator>(() => WabiSabiCoordinator, "WabiSabi Coordinator"); HostedServices.Register <RoundBootstrapper>(() => new RoundBootstrapper(TimeSpan.FromMilliseconds(100), Coordinator), "Round Bootstrapper"); await HostedServices.StartAllAsync(cancel); IndexBuilderService = new(RpcClient, blockNotifier, indexFilePath); IndexBuilderService.Synchronize(); Logger.LogInfo($"{nameof(IndexBuilderService)} is successfully initialized and started synchronization."); }
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 WrongPhaseException(round, Phase.InputRegistration); } 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(SecureRandom.Instance.GetBytes(16)); var isPayingZeroCoordinationFee = CoinJoinIdStore.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 => CoinJoinIdStore.Contains(input.PrevOut.Hash))) { isPayingZeroCoordinationFee = true; } } var alice = new Alice(coin, request.OwnershipProof, round, id, isPayingZeroCoordinationFee); if (alice.CalculateRemainingAmountCredentials(round.Parameters.MiningFeeRate, round.Parameters.CoordinationFeeRate) <= Money.Zero) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.UneconomicalInput); } if (alice.TotalInputAmount < round.Parameters.MinAmountCredentialValue) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.NotEnoughFunds); } if (alice.TotalInputAmount > round.Parameters.MaxAmountCredentialValue) { throw new WabiSabiProtocolException(WabiSabiProtocolErrorCode.TooMuchFunds); } if (alice.TotalInputVsize > round.Parameters.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.Parameters.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)); } }