CreateRoundWithOutputsReadyToSignAsync(Arena arena, Key key1, SmartCoin coin1, Key key2, SmartCoin coin2) { // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var round = Assert.Single(arena.Rounds); round.MaxVsizeAllocationPerAlice = 11 + 31 + MultipartyTransactionParameters.SharedOverhead; var arenaClient = WabiSabiFactory.CreateArenaClient(arena); // Register Alices. using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), arena); await roundStateUpdater.StartAsync(CancellationToken.None); using var identificationKey = new Key(); var task1 = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, coin1, key1.GetBitcoinSecret(round.Network), identificationKey, roundStateUpdater, CancellationToken.None); var task2 = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, coin2, key2.GetBitcoinSecret(round.Network), identificationKey, roundStateUpdater, CancellationToken.None); while (Phase.OutputRegistration != round.Phase) { await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); } await Task.WhenAll(task1, task2); var aliceClient1 = task1.Result; var aliceClient2 = task2.Result; // Register outputs. var bobClient = new BobClient(round.Id, arenaClient); using var destKey1 = new Key(); using var destKey2 = new Key(); await bobClient.RegisterOutputAsync( destKey1.PubKey.WitHash.ScriptPubKey, aliceClient1.IssuedAmountCredentials.Take(ProtocolConstants.CredentialNumber), aliceClient1.IssuedVsizeCredentials.Take(ProtocolConstants.CredentialNumber), CancellationToken.None).ConfigureAwait(false); await bobClient.RegisterOutputAsync( destKey1.PubKey.WitHash.ScriptPubKey, aliceClient2.IssuedAmountCredentials.Take(ProtocolConstants.CredentialNumber), aliceClient2.IssuedVsizeCredentials.Take(ProtocolConstants.CredentialNumber), CancellationToken.None).ConfigureAwait(false); await roundStateUpdater.StopAsync(CancellationToken.None); return(round, aliceClient1, aliceClient2); }
public void Test1() { var registry = new CoinsRegistry(); var key0 = new Key(); var key1 = new Key(); var stx0 = TransactionBuilders.CreateCreditingTransaction(key0.PubKey.Hash, Money.Coins(2)); var stx1 = TransactionBuilders.CreateCreditingTransaction(key1.PubKey.Hash, Money.Coins(5)); var coin0 = new SmartCoin(stx0, 0); Assert.True(registry.TryAdd(coin0)); Assert.False(registry.TryAdd(coin0)); Assert.False(registry.TryAdd(coin0.AsSpentBy(uint256.One))); var registeredCoin = Assert.Single(registry.AsCoinsView()); Assert.Equal(registeredCoin, coin0); Assert.True(registeredCoin.Unspent); var coin1 = new SmartCoin(stx1, 0); Assert.True(registry.TryAdd(coin1)); //////////// var(key2, keyChange) = (new Key(), new Key()); var stx2 = TransactionBuilders.CreateSpendingTransaction(new[] { coin0.GetCoin(), coin1.GetCoin() }, key2.PubKey.Hash, keyChange.PubKey.Hash); registry.Spend(coin0, stx2.GetHash()); registry.Spend(coin1, stx2.GetHash()); var spentCoin = Assert.Single(registry.AsCoinsView(), coin => coin == coin0); Assert.False(spentCoin.Unspent); Assert.Equal(spentCoin, coin0); var coin2 = new SmartCoin(stx2, 0); var coin3 = new SmartCoin(stx2, 1); Assert.True(registry.TryAdd(coin2)); Assert.True(registry.TryAdd(coin3)); //////////// var changes = registry.Undo(stx0.GetHash()); Assert.Empty(registry); var removedCoin = Assert.Single(changes.toRemove); Assert.Empty(changes.toAdd); }
public CoinViewModel(SmartCoin model) { Model = model; model.WhenAnyValue(x => x.Confirmed).ObserveOn(RxApp.MainThreadScheduler).Subscribe(confirmed => { RefreshSmartCoinStatus(); this.RaisePropertyChanged(nameof(Confirmed)); }); model.WhenAnyValue(x => x.SpentOrCoinJoinInProgress).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { this.RaisePropertyChanged(nameof(SpentOrCoinJoinInProgress)); }); model.WhenAnyValue(x => x.CoinJoinInProgress).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { RefreshSmartCoinStatus(); this.RaisePropertyChanged(nameof(CoinJoinInProgress)); }); model.WhenAnyValue(x => x.IsBanned).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { RefreshSmartCoinStatus(); }); model.WhenAnyValue(x => x.SpentAccordingToBackend).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { RefreshSmartCoinStatus(); }); model.WhenAnyValue(x => x.Unspent).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { this.RaisePropertyChanged(nameof(Unspent)); }); this.WhenAnyValue(x => x.Status).Subscribe(_ => { this.RaisePropertyChanged(nameof(ToolTip)); }); Global.Synchronizer.WhenAnyValue(x => x.BestBlockchainHeight).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { RefreshSmartCoinStatus(); this.RaisePropertyChanged(nameof(Confirmations)); }); Global.ChaumianClient.StateUpdated += ChaumianClient_StateUpdated; }
public CoinViewModel(ChaincaseWalletManager walletManager, Config config, BitcoinStore bitcoinStore, SmartCoin model) { _walletManager = walletManager; _config = config; _bitcoinStore = bitcoinStore; Model = model; Disposables = new CompositeDisposable(); _coinJoinInProgress = Model.WhenAnyValue(x => x.CoinJoinInProgress) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.CoinJoinInProgress) .DisposeWith(Disposables); _unspent = Model.WhenAnyValue(x => x.Unspent).ToProperty(this, x => x.Unspent, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _confirmed = Model.WhenAnyValue(x => x.Confirmed).ToProperty(this, x => x.Confirmed, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); _unavailable = Model.WhenAnyValue(x => x.Unavailable).ToProperty(this, x => x.Unavailable, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); this.WhenAnyValue(x => x.Status) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(ToolTip))); _cluster = Model .WhenAnyValue(x => x.Clusters, x => x.Clusters.Labels) .Select(x => x.Item2.ToString()) .ToProperty(this, x => x.Clusters, scheduler: RxApp.MainThreadScheduler) .DisposeWith(Disposables); Observable .Merge(Model.WhenAnyValue(x => x.IsBanned, x => x.SpentAccordingToBackend, x => x.CoinJoinInProgress).Select(_ => Unit.Default)) .Merge(Observable.FromEventPattern(_walletManager.CurrentWallet.ChaumianClient, nameof(_walletManager.CurrentWallet.ChaumianClient.StateUpdated)).Select(_ => Unit.Default)) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => RefreshSmartCoinStatus()) .DisposeWith(Disposables); _bitcoinStore.SmartHeaderChain .WhenAnyValue(x => x.TipHeight).Select(_ => Unit.Default) .Merge(Model.WhenAnyValue(x => x.Height).Select(_ => Unit.Default)) .Throttle(TimeSpan.FromSeconds(0.1)) // DO NOT TAKE THIS THROTTLE OUT, OTHERWISE SYNCING WITH COINS IN THE WALLET WILL STACKOVERFLOW! .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.RaisePropertyChanged(nameof(Confirmations))) .DisposeWith(Disposables); }
/// <returns>If the selection was successful. If there's enough coins to spend from.</returns> private bool TrySelectCoins(HashSet <SmartCoin> coinsToSpend, Money totalOutAmount, IEnumerable <SmartCoin> unspentCoins) { // If there's no need for input merging, then use the largest selected. // Do not prefer anonymity set. You can assume the user prefers anonymity set manually through the GUI. SmartCoin largestCoin = unspentCoins.OrderByDescending(x => x.Amount).FirstOrDefault(); if (largestCoin == default) { return(false); // If there's no coin then unsuccessful selection. } else // Check if we can do without input merging. { var largestCoins = unspentCoins.Where(x => x.ScriptPubKey == largestCoin.ScriptPubKey); if (largestCoins.Sum(x => x.Amount) >= totalOutAmount) { foreach (var c in largestCoins) { coinsToSpend.Add(c); } return(true); } } // If there's a need for input merging. foreach (var coin in unspentCoins .OrderByDescending(x => x.AnonymitySet) // Always try to spend/merge the largest anonset coins first. .ThenByDescending(x => x.Amount)) // Then always try to spend by amount. { coinsToSpend.Add(coin); // If reaches the amount, then return true, else just go with the largest coin. if (coinsToSpend.Select(x => x.Amount).Sum() >= totalOutAmount) { // Add if we can find address reuse. foreach (var c in unspentCoins .Except(coinsToSpend) // So we're choosing from the non selected coins. .Where(x => coinsToSpend.Any(y => y.ScriptPubKey == x.ScriptPubKey))) // Where the selected coins contains the same script. { coinsToSpend.Add(c); } return(true); } } return(false); }
public async Task CreateNewAsync() { var config = new WabiSabiConfig { MaxInputCountByRound = 1 }; var round = WabiSabiFactory.CreateRound(config); var km = ServiceFactory.CreateKeyManager(""); var key = BitcoinFactory.CreateHdPubKey(km); SmartCoin coin1 = BitcoinFactory.CreateSmartCoin(key, Money.Coins(1m)); var mockRpc = WabiSabiFactory.CreatePreconfiguredRpcClient(coin1.Coin); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, mockRpc, round); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); var wabiSabiApi = new WabiSabiController(coordinator); var insecureRandom = new InsecureRandom(); var roundState = RoundState.FromRound(round); var arenaClient = new ArenaClient( roundState.CreateAmountCredentialClient(insecureRandom), roundState.CreateVsizeCredentialClient(insecureRandom), wabiSabiApi); Assert.Equal(Phase.InputRegistration, arena.Rounds.First().Phase); var bitcoinSecret = km.GetSecrets("", coin1.ScriptPubKey).Single().PrivateKey.GetBitcoinSecret(Network.Main); var aliceClient = new AliceClient(round.Id, arenaClient, coin1.Coin, round.FeeRate, bitcoinSecret); await aliceClient.RegisterInputAsync(CancellationToken.None); using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), wabiSabiApi); Task confirmationTask = aliceClient.ConfirmConnectionAsync( TimeSpan.FromSeconds(1), new long[] { coin1.EffectiveValue(round.FeeRate) }, new long[] { roundState.MaxVsizeAllocationPerAlice - coin1.ScriptPubKey.EstimateInputVsize() }, roundStateUpdater, CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await confirmationTask; Assert.Equal(Phase.ConnectionConfirmation, arena.Rounds.First().Phase); }
private void RemoveCoin(SmartCoin coinWaitingForMix) { State.RemoveCoinFromWaitingList(coinWaitingForMix); coinWaitingForMix.CoinJoinInProgress = false; coinWaitingForMix.Secret = null; if (coinWaitingForMix.Label == "ZeroLink Change" && coinWaitingForMix.Unspent) { coinWaitingForMix.Label = "ZeroLink Dequeued Change"; var key = KeyManager.GetKeys(x => x.GetP2wpkhScript() == coinWaitingForMix.ScriptPubKey).SingleOrDefault(); if (!(key is null)) { key.SetLabel(coinWaitingForMix.Label, KeyManager); } } CoinDequeued?.Invoke(this, coinWaitingForMix); Logger.LogInfo <CcjClient>($"Coin dequeued: {coinWaitingForMix.Index}:{coinWaitingForMix.TransactionId}."); }
public async Task CreateNewAsync() { var config = new WabiSabiConfig { MaxInputCountByRound = 1 }; var round = WabiSabiFactory.CreateRound(config); using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); var km = ServiceFactory.CreateKeyManager(""); var key = BitcoinFactory.CreateHdPubKey(km); SmartCoin coin1 = BitcoinFactory.CreateSmartCoin(key, Money.Coins(1m)); var outpoint = coin1.OutPoint; var mockRpc = new Mock <IRPCClient>(); mockRpc.Setup(rpc => rpc.GetTxOutAsync(outpoint.Hash, (int)outpoint.N, true)) .ReturnsAsync(new NBitcoin.RPC.GetTxOutResponse { IsCoinBase = false, Confirmations = coin1.Height, TxOut = coin1.TxOut, }); await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object); CredentialPool amountCredentialPool = new(); CredentialPool weightCredentialPool = new(); var arenaClient = new ArenaClient(round.AmountCredentialIssuerParameters, round.WeightCredentialIssuerParameters, amountCredentialPool, weightCredentialPool, coordinator, new InsecureRandom()); Assert.Equal(Phase.InputRegistration, arena.Rounds.First().Value.Phase); var bitcoinSecret = km.GetSecrets("", coin1.ScriptPubKey).Single().PrivateKey.GetBitcoinSecret(Network.Main); var aliceClient = await AliceClient.CreateNewAsync(arenaClient, new[] { coin1.Coin }, bitcoinSecret, round.Id, round.Hash, round.FeeRate); Task confirmationTask = aliceClient.ConfirmConnectionAsync(TimeSpan.FromSeconds(3), CancellationToken.None); await arena.TriggerAndWaitRoundAsync(TimeSpan.FromMinutes(1)); await confirmationTask; Assert.Equal(Phase.ConnectionConfirmation, arena.Rounds.First().Value.Phase); }
public void ReceiveTransactionForWallet() { var transactionProcessor = CreateTransactionProcessor(); SmartCoin receivedCoin = null; transactionProcessor.CoinReceived += (s, theCoin) => receivedCoin = theCoin; var keys = transactionProcessor.KeyManager.GetKeys(); var tx = CreateCreditingTransaction(keys.First().P2wpkhScript, Money.Coins(1.0m)); var relevant = transactionProcessor.Process(tx); // It is relevant because is funding the wallet Assert.True(relevant); var coin = Assert.Single(transactionProcessor.Coins); Assert.Equal(Money.Coins(1.0m), coin.Amount); Assert.Contains(transactionProcessor.TransactionCache, x => x == tx); Assert.NotNull(receivedCoin); }
public static async Task <AliceClient> CreateRegisterAndConfirmInputAsync( RoundState roundState, ArenaClient arenaClient, SmartCoin coin, BitcoinSecret bitcoinSecret, RoundStateUpdater roundStatusUpdater, CancellationToken cancellationToken) { AliceClient?aliceClient = null; try { aliceClient = await RegisterInputAsync(roundState, arenaClient, coin, bitcoinSecret, cancellationToken).ConfigureAwait(false); await aliceClient.ConfirmConnectionAsync(roundStatusUpdater, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { if (aliceClient is { })
public CoinViewModel(SmartCoin model) { Model = model; model.WhenAnyValue(x => x.Confirmed).ObserveOn(RxApp.MainThreadScheduler).Subscribe(confirmed => { this.RaisePropertyChanged(nameof(Confirmed)); }); model.WhenAnyValue(x => x.SpentOrCoinJoinInProgress).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { this.RaisePropertyChanged(nameof(SpentOrCoinJoinInProgress)); }); model.WhenAnyValue(x => x.CoinJoinInProgress).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ => { this.RaisePropertyChanged(nameof(CoinJoinInProgress)); }); }
public bool TryAdd(SmartCoin coin) { var added = false; lock (Lock) { if (!SpentCoins.Contains(coin)) { added = Coins.Add(coin); coin.RegisterToHdPubKey(); if (added) { foreach (var outPoint in coin.Transaction.Transaction.Inputs.Select(x => x.PrevOut)) { var newCoinSet = new HashSet <SmartCoin> { coin }; // If we don't succeed to add a new entry to the dictionary. if (!CoinsByOutPoint.TryAdd(outPoint, newCoinSet)) { var previousCoinTxId = CoinsByOutPoint[outPoint].First().TransactionId; // Then check if we're in the same transaction as the previous coins in the dictionary are. if (coin.TransactionId == previousCoinTxId) { // If we are in the same transaction, then just add it to value set. CoinsByOutPoint[outPoint].Add(coin); } else { // If we aren't in the same transaction, then it's a conflict, so replace the old set with the new one. CoinsByOutPoint[outPoint] = newCoinSet; } } } InvalidateSnapshot = true; } } } return(added); }
public async Task ReceiveTransactionForWalletAsync() { var transactionProcessor = await CreateTransactionProcessorAsync(); SmartCoin receivedCoin = null; transactionProcessor.CoinReceived += (s, theCoin) => receivedCoin = theCoin; var keys = transactionProcessor.KeyManager.GetKeys(); var tx = CreateCreditingTransaction(keys.First().P2wpkhScript, Money.Coins(1.0m)); var relevant = transactionProcessor.Process(tx); // It is relevant because is funding the wallet Assert.True(relevant); var coin = Assert.Single(transactionProcessor.Coins); Assert.Equal(Money.Coins(1.0m), coin.Amount); Assert.True(transactionProcessor.TransactionStore.TryGetTransaction(tx.GetHash(), out _)); Assert.NotNull(receivedCoin); }
private void RemoveCoin(SmartCoin coinWaitingForMix, string reason = null) { State.RemoveCoinFromWaitingList(coinWaitingForMix); coinWaitingForMix.CoinJoinInProgress = false; coinWaitingForMix.Secret = null; if (coinWaitingForMix.Label == "ZeroLink Change" && coinWaitingForMix.Unspent) { coinWaitingForMix.Label = "ZeroLink Dequeued Change"; var key = KeyManager.GetKeys(x => x.P2wpkhScript == coinWaitingForMix.ScriptPubKey).SingleOrDefault(); if (key != null) { key.SetLabel(coinWaitingForMix.Label, KeyManager); } } CoinDequeued?.Invoke(this, coinWaitingForMix); var correctReason = Guard.Correct(reason); var reasonText = correctReason != "" ? $" Reason: {correctReason}" : ""; Logger.LogInfo <CcjClient>($"Coin dequeued: {coinWaitingForMix.Index}:{coinWaitingForMix.TransactionId}.{reasonText}"); }
private AliceClient( Guid aliceId, RoundState roundState, ArenaClient arenaClient, SmartCoin coin, BitcoinSecret bitcoinSecret, IEnumerable <Credential> issuedAmountCredentials, IEnumerable <Credential> issuedVsizeCredentials) { AliceId = aliceId; RoundId = roundState.Id; ArenaClient = arenaClient; SmartCoin = coin; FeeRate = roundState.FeeRate; BitcoinSecret = bitcoinSecret; IssuedAmountCredentials = issuedAmountCredentials; IssuedVsizeCredentials = issuedVsizeCredentials; MaxVsizeAllocationPerAlice = roundState.MaxVsizeAllocationPerAlice; ConfirmationTimeout = roundState.ConnectionConfirmationTimeout / 2; }
public static async Task <AliceClient> CreateRegisterAndConfirmInputAsync( RoundState roundState, ArenaClient arenaClient, SmartCoin coin, IKeyChain keyChain, RoundStateUpdater roundStatusUpdater, CancellationToken cancellationToken) { AliceClient?aliceClient = null; try { aliceClient = await RegisterInputAsync(roundState, arenaClient, coin, keyChain, cancellationToken).ConfigureAwait(false); await aliceClient.ConfirmConnectionAsync(roundStatusUpdater, cancellationToken).ConfigureAwait(false); Logger.LogInfo($"Round ({aliceClient.RoundId}), Alice ({aliceClient.AliceId}): Connection was confirmed."); } catch (OperationCanceledException) { if (aliceClient is { })
public void OnlyOneNonPrivateCoinInBigSetOfCoinsConsolidationMode() { const int MinAnonimitySet = 10; var km = KeyManager.CreateNew(out _, "", Network.Main); SmartCoin smallerAnonCoin = BitcoinFactory.CreateSmartCoin(BitcoinFactory.CreateHdPubKey(km), Money.Coins(1m), 0, anonymitySet: MinAnonimitySet - 1); var coinsToSelectFrom = Enumerable .Range(0, 10) .Select(i => BitcoinFactory.CreateSmartCoin(BitcoinFactory.CreateHdPubKey(km), Money.Coins(1m), 0, anonymitySet: MinAnonimitySet + 1)) .Prepend(smallerAnonCoin) .ToList(); var coins = CoinJoinClient.SelectCoinsForRound( coins: coinsToSelectFrom, CreateMultipartyTransactionParameters(), consolidationMode: true, minAnonScoreTarget: MinAnonimitySet, ConfigureRng(5)); Assert.Contains(smallerAnonCoin, coins); Assert.Equal(10, coins.Count); }
CreateRoundWithTwoConfirmedConnectionsAsync(Arena arena, Key key1, SmartCoin coin1, Key key2, SmartCoin coin2) { // Create the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var arenaClient = WabiSabiFactory.CreateArenaClient(arena); var round = Assert.Single(arena.Rounds); round.MaxVsizeAllocationPerAlice = 11 + 31 + MultipartyTransactionParameters.SharedOverhead; using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), arena); await roundStateUpdater.StartAsync(CancellationToken.None); var identificationKey = new Key(); var task1 = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, coin1, key1.GetBitcoinSecret(round.Network), identificationKey, roundStateUpdater, CancellationToken.None); var task2 = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, coin2, key2.GetBitcoinSecret(round.Network), identificationKey, roundStateUpdater, CancellationToken.None); while (Phase.ConnectionConfirmation != round.Phase) { await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); } await Task.WhenAll(task1, task2); var aliceClient1 = task1.Result; var aliceClient2 = task2.Result; await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); await roundStateUpdater.StopAsync(CancellationToken.None); return(round, arenaClient, new[] { aliceClient1, aliceClient2 }); }
CreateRoundWithTwoConfirmedConnectionsAsync(Arena arena, IKeyChain keyChain, SmartCoin coin1, SmartCoin coin2) { // Get the round. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); var arenaClient = WabiSabiFactory.CreateArenaClient(arena); var round = Assert.Single(arena.Rounds); // Refresh the Arena States because of vsize manipulation. await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); using RoundStateUpdater roundStateUpdater = new(TimeSpan.FromSeconds(2), arena); await roundStateUpdater.StartAsync(CancellationToken.None); var task1 = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, coin1, keyChain, roundStateUpdater, CancellationToken.None); var task2 = AliceClient.CreateRegisterAndConfirmInputAsync(RoundState.FromRound(round), arenaClient, coin2, keyChain, roundStateUpdater, CancellationToken.None); while (Phase.ConnectionConfirmation != round.Phase) { await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); } await Task.WhenAll(task1, task2); var aliceClient1 = await task1; var aliceClient2 = await task2; await arena.TriggerAndWaitRoundAsync(TimeSpan.FromSeconds(21)); Assert.Equal(Phase.OutputRegistration, round.Phase); await roundStateUpdater.StopAsync(CancellationToken.None); return(round, arenaClient, new[] { aliceClient1, aliceClient2 }); }
private AliceClient( Guid aliceId, RoundState roundState, ArenaClient arenaClient, SmartCoin coin, OwnershipProof ownershipProof, IEnumerable <Credential> issuedAmountCredentials, IEnumerable <Credential> issuedVsizeCredentials, bool isPayingZeroCoordinationFee) { AliceId = aliceId; RoundId = roundState.Id; ArenaClient = arenaClient; SmartCoin = coin; OwnershipProof = ownershipProof; FeeRate = roundState.FeeRate; CoordinationFeeRate = roundState.CoordinationFeeRate; IssuedAmountCredentials = issuedAmountCredentials; IssuedVsizeCredentials = issuedVsizeCredentials; MaxVsizeAllocationPerAlice = roundState.MaxVsizeAllocationPerAlice; ConfirmationTimeout = roundState.ConnectionConfirmationTimeout / 2; IsPayingZeroCoordinationFee = isPayingZeroCoordinationFee; }
private async Task BroadcastTransactionToBackendAsync(SmartTransaction transaction) { Logger.LogInfo("Broadcasting with backend..."); using (var client = new WasabiClient(Synchronizer.WasabiClient.TorClient.DestinationUriAction, Synchronizer.WasabiClient.TorClient.TorSocks5EndPoint)) { try { await client.BroadcastAsync(transaction).ConfigureAwait(false); } catch (HttpRequestException ex2) when(ex2.Message.Contains("bad-txns-inputs-missingorspent", StringComparison.InvariantCultureIgnoreCase) || ex2.Message.Contains("missing-inputs", StringComparison.InvariantCultureIgnoreCase) || ex2.Message.Contains("txn-mempool-conflict", StringComparison.InvariantCultureIgnoreCase)) { if (transaction.Transaction.Inputs.Count == 1) // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know. { OutPoint input = transaction.Transaction.Inputs.First().PrevOut; lock (WalletServicesLock) { foreach (var walletService in AliveWalletServices) { SmartCoin coin = walletService.Coins.GetByOutPoint(input); if (coin != default) { coin.SpentAccordingToBackend = true; } } } } throw; } } BelieveTransaction(transaction); Logger.LogInfo($"Transaction is successfully broadcasted to backend: {transaction.GetHash()}."); }
public void SmartCoinSerialization() { var tx = Transaction.Parse("02000000040aa8d0af84518df6e3a60c5bb19d9c3fcc3dc6e26b2f2449e8d7bf8d3fe84b87010000006a473044022018dfe9216c1209dd6c2b6c1607dbac4e499c1fce4878bc7d5d83fccbf3e24c9402202cac351c9c6a2b5eef338cbf0ec000d8de1c05e96a904cbba2b9e6ffc2d4e19501210364cc39da1091b1a9c12ec905a14a9e8478f951f7a1accdabeb40180533f2eaa5feffffff112c07d0f5e0617d720534f0b2b84dc0d5b7314b358c3ab338823b9e5bfbddf5010000006b483045022100ec155e7141e74661ee511ae980150a6c89261f31070999858738369afc28f2b6022006230d2aa24fac110b74ef15b84371486cf76c539b335a253c14462447912a300121020c2f41390f031d471b22abdb856e6cdbe0f4d74e72c197469bfd54e5a08f7e67feffffff38e799b8f6cf04fd021a9b135cdcd347da7aac4fd8bb8d0da9316a9fb228bb6e000000006b483045022100fc1944544a3f96edd8c8a9795c691e2725612b5ab2e1c999be11a2a4e3f841f1022077b2e088877829edeada0c707a9bb577aa79f26dafacba3d1d2d047f52524296012102e6015963dff9826836400cf8f45597c0705757d5dcdc6bf734f661c7dab89e69feffffff64c3f0377e86625123f2f1ee229319ed238e8ca8b7dda5bc080a2c5ecb984629000000006a47304402204233a90d6296182914424fd2901e16e6f5b13b451b67b0eec25a5eaacc5033c902203d8a13ef0b494c12009663475458e51da6bd55cc67688264230ece81d3eeca24012102f806d7152da2b52c1d9ad928e4a6253ccba080a5b9ab9efdd80e37274ac67f9bfeffffff0290406900000000001976a91491ac4e49b66f845180d98d8f8be6121588be6e3b88ac52371600000000001976a9142f44ed6749e8c84fd476e4440741f7e6f55542fa88acadd30700", Network.TestNet); var txId = tx.GetHash(); var index = 0U; var output = tx.Outputs[0]; var scriptPubKey = output.ScriptPubKey; var amount = output.Value; var spentOutputs = tx.Inputs.ToTxoRefs().ToArray(); var height = Height.MemPool; var label = "foo"; var bannedUntil = DateTimeOffset.UtcNow; var coin = new SmartCoin(txId, index, scriptPubKey, amount, spentOutputs, height, tx.RBF, tx.GetMixin(index), label, txId); coin.BannedUntilUtc = bannedUntil; var serialized = JsonConvert.SerializeObject(coin); var deserialized = JsonConvert.DeserializeObject <SmartCoin>(serialized); Assert.Equal(coin, deserialized); Assert.Equal(coin.Height, deserialized.Height); Assert.Equal(coin.Amount, deserialized.Amount); Assert.Equal(coin.Index, deserialized.Index); Assert.Equal(coin.SpentOrCoinJoinInProgress, deserialized.SpentOrCoinJoinInProgress); Assert.Equal(coin.Label, deserialized.Label); Assert.Equal(coin.ScriptPubKey, deserialized.ScriptPubKey); Assert.Equal(coin.SpenderTransactionId, deserialized.SpenderTransactionId); Assert.Equal(coin.TransactionId, deserialized.TransactionId); Assert.Equal(coin.SpentOutputs.Length, deserialized.SpentOutputs.Length); Assert.Equal(coin.BannedUntilUtc, deserialized.BannedUntilUtc); for (int i = 0; i < coin.SpentOutputs.Length; i++) { Assert.Equal(coin.SpentOutputs[0], deserialized.SpentOutputs[0]); } }
public void SpendCoin() { var transactionProcessor = CreateTransactionProcessor(); SmartCoin spentCoin = null; transactionProcessor.CoinSpent += (s, theCoin) => spentCoin = theCoin; var keys = transactionProcessor.KeyManager.GetKeys(); var tx = CreateCreditingTransaction(keys.First().P2wpkhScript, Money.Coins(1.0m)); transactionProcessor.Process(tx); var createdCoin = tx.Transaction.Outputs.AsCoins().First(); // Spend the received coin tx = CreateSpendingTransaction(createdCoin, new Key().PubKey.ScriptPubKey); var relevant = transactionProcessor.Process(tx); Assert.True(relevant); var coin = Assert.Single(transactionProcessor.Coins); Assert.False(coin.Unspent); Assert.NotNull(spentCoin); Assert.Equal(coin, spentCoin); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { _model = model; this.WhenAnyValue(x => x.IsSelected).Subscribe(selected => { if (selected) { if (!owner.SelectedCoins.Contains(this)) { owner.SelectedCoins.Add(this); } } else { owner.SelectedCoins.Remove(this); } }); model.WhenAnyValue(x => x.Confirmed).ObserveOn(RxApp.MainThreadScheduler).Subscribe(confirmed => { this.RaisePropertyChanged(nameof(Confirmed)); }); }
public CoinViewModel(CoinListViewModel owner, SmartCoin model) { Model = model; _owner = owner; }
public bool Process(SmartTransaction tx) { if (!tx.Transaction.PossiblyP2WPKHInvolved()) { return(false); // We do not care about non-witness transactions for other than mempool cleanup. } uint256 txId = tx.GetHash(); var walletRelevant = false; if (tx.Confirmed) { foreach (var coin in Coins.Where(x => x.TransactionId == txId)) { coin.Height = tx.Height; walletRelevant = true; // relevant } } if (!tx.Transaction.IsCoinBase) // Transactions we already have and processed would be "double spends" but they shouldn't. { var doubleSpends = new List <SmartCoin>(); foreach (SmartCoin coin in Coins) { var spent = false; foreach (TxoRef spentOutput in coin.SpentOutputs) { foreach (TxIn txIn in tx.Transaction.Inputs) { if (spentOutput.TransactionId == txIn.PrevOut.Hash && spentOutput.Index == txIn.PrevOut.N) // Do not do (spentOutput == txIn.PrevOut), it's faster this way, because it won't check for null. { doubleSpends.Add(coin); spent = true; walletRelevant = true; break; } } if (spent) { break; } } } if (doubleSpends.Any()) { if (tx.Height == Height.Mempool) { // if the received transaction is spending at least one input already // spent by a previous unconfirmed transaction signaling RBF then it is not a double // spanding transaction but a replacement transaction. if (doubleSpends.Any(x => x.IsReplaceable)) { // remove double spent coins (if other coin spends it, remove that too and so on) // will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends.Where(x => !x.Confirmed)) { Coins.TryRemove(doubleSpentCoin); } tx.SetReplacement(); walletRelevant = true; } else { return(false); } } else // new confirmation always enjoys priority { // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys foreach (SmartCoin doubleSpentCoin in doubleSpends) { Coins.TryRemove(doubleSpentCoin); } walletRelevant = true; } } } var isLikelyCoinJoinOutput = false; bool hasEqualOutputs = tx.Transaction.GetIndistinguishableOutputs(includeSingle: false).FirstOrDefault() != default; if (hasEqualOutputs) { var receiveKeys = KeyManager.GetKeys(x => tx.Transaction.Outputs.Any(y => y.ScriptPubKey == x.P2wpkhScript)); bool allReceivedInternal = receiveKeys.All(x => x.IsInternal); if (allReceivedInternal) { // It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs. Money spentAmount = Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).Sum(x => x.Amount); Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value); bool receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m)); if (receivedAlmostAsMuchAsSpent) { isLikelyCoinJoinOutput = true; } } } List <SmartCoin> spentOwnCoins = null; for (var i = 0U; i < tx.Transaction.Outputs.Count; i++) { // If transaction received to any of the wallet keys: var output = tx.Transaction.Outputs[i]; HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey); if (foundKey != default) { walletRelevant = true; if (output.Value <= DustThreshold) { continue; } foundKey.SetKeyState(KeyState.Used, KeyManager); spentOwnCoins ??= Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList(); var anonset = tx.Transaction.GetAnonymitySet(i); if (spentOwnCoins.Count != 0) { anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1; // Minus 1, because do not count own. } SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, isLikelyCoinJoinOutput, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey); // Do not inherit locked status from key, that's different. // If we did not have it. if (Coins.TryAdd(newCoin)) { CoinReceived?.Invoke(this, newCoin); // Make sure there's always 21 clean keys generated and indexed. KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal); if (foundKey.IsInternal) { // Make sure there's always 14 internal locked keys generated and indexed. KeyManager.AssertLockedInternalKeysIndexed(14); } } else // If we had this coin already. { if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had. { SmartCoin oldCoin = Coins.FirstOrDefault(x => x.TransactionId == txId && x.Index == i); if (oldCoin != null) // Just to be sure, it is a concurrent collection. { oldCoin.Height = newCoin.Height; } } } } } // If spends any of our coin for (var i = 0; i < tx.Transaction.Inputs.Count; i++) { var input = tx.Transaction.Inputs[i]; var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N); if (foundCoin != null) { walletRelevant = true; var alreadyKnown = foundCoin.SpenderTransactionId == txId; foundCoin.SpenderTransactionId = txId; if (!alreadyKnown) { CoinSpent?.Invoke(this, foundCoin); } if (tx.Confirmed) { SpenderConfirmed?.Invoke(this, foundCoin); } } } if (walletRelevant) { TransactionStore.AddOrUpdate(tx); } return(walletRelevant); }
private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound) { try { List <(uint256 txid, uint index)> registrableCoins = State.GetRegistrableCoins( inputRegistrableRound.State.MaximumInputCountPerPeer, inputRegistrableRound.State.Denomination, inputRegistrableRound.State.FeePerInputs, inputRegistrableRound.State.FeePerOutputs).ToList(); if (registrableCoins.Any()) { BitcoinAddress changeAddress = null; BitcoinAddress activeAddress = null; lock (CustomChangeAddressesLock) { if (CustomChangeAddresses.Count > 0) { changeAddress = CustomChangeAddresses.First(); CustomChangeAddresses.RemoveFirst(); } } lock (CustomActiveAddressesLock) { if (CustomActiveAddresses.Count > 0) { activeAddress = CustomActiveAddresses.First(); CustomActiveAddresses.RemoveFirst(); } } if (changeAddress is null || activeAddress is null) { IEnumerable <HdPubKey> allUnusedInternalKeys = KeyManager.GetKeys(keyState: null, isInternal: true).Where(x => x.KeyState != KeyState.Used); if (changeAddress is null) { string changeLabel = "ZeroLink Change"; IEnumerable <HdPubKey> allChangeKeys = allUnusedInternalKeys.Where(x => x.Label == changeLabel); HdPubKey changeKey = null; KeyManager.AssertLockedInternalKeysIndexed(14); IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys); if (allChangeKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any()) // Then don't generate new keys, because it'd bloat the wallet. { // Find the first one that we did not try to register in the current session. changeKey = allChangeKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x)); // If there is no such a key, then use the oldest. if (changeKey == default) { changeKey = AccessCache.Where(x => allChangeKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key; } changeKey.SetLabel(changeLabel); changeKey.SetKeyState(KeyState.Locked); } else { changeKey = internalNotCachedLockedKeys.RandomElement(); changeKey.SetLabel(changeLabel); } changeAddress = changeKey.GetP2wpkhAddress(Network); AccessCache.AddOrReplace(changeKey, DateTimeOffset.UtcNow); } if (activeAddress is null) { string activeLabel = "ZeroLink Mixed Coin"; IEnumerable <HdPubKey> allActiveKeys = allUnusedInternalKeys.Where(x => x.Label == activeLabel); HdPubKey activeKey = null; KeyManager.AssertLockedInternalKeysIndexed(14); IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys); if (allActiveKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any()) // Then don't generate new keys, because it'd bloat the wallet. { // Find the first one that we did not try to register in the current session. activeKey = allActiveKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x)); // If there is no such a key, then use the oldest. if (activeKey == default) { activeKey = AccessCache.Where(x => allActiveKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key; } activeKey.SetLabel(activeLabel); activeKey.SetKeyState(KeyState.Locked); activeAddress = activeKey.GetP2wpkhAddress(Network); } else { activeKey = internalNotCachedLockedKeys.RandomElement(); activeKey.SetLabel(activeLabel); } activeAddress = activeKey.GetP2wpkhAddress(Network); AccessCache.AddOrReplace(activeKey, DateTimeOffset.UtcNow); } } KeyManager.ToFile(); var blind = CoordinatorPubKey.Blind(activeAddress.ScriptPubKey.ToBytes()); var inputProofs = new List <InputProofModel>(); foreach ((uint256 txid, uint index)coinReference in registrableCoins) { SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } coin.Secret = coin.Secret ?? KeyManager.GetSecrets(OnePiece, coin.ScriptPubKey).Single(); var inputProof = new InputProofModel { Input = coin.GetTxoRef(), Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData)) }; inputProofs.Add(inputProof); } AliceClient aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint); byte[] unblindedSignature = CoordinatorPubKey.UnblindSignature(aliceClient.BlindedOutputSignature, blind.BlindingFactor); if (!CoordinatorPubKey.Verify(unblindedSignature, activeAddress.ScriptPubKey.ToBytes())) { throw new NotSupportedException("Coordinator did not sign the blinded output properly."); } CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId); if (roundRegistered is null) { // If our SatoshiClient doesn't yet know about the round because of the dealy create it. // Make its state as it'd be the same as our assumed round was, except the roundId and registeredPeerCount, it'll be updated later. roundRegistered = new CcjClientRound(CcjRunningRoundState.CloneExcept(inputRegistrableRound.State, aliceClient.RoundId, registeredPeerCount: 1)); State.AddOrReplaceRound(roundRegistered); } foreach ((uint256 txid, uint index)coinReference in registrableCoins) { var coin = State.GetSingleOrDefaultFromWaitingList(coinReference); if (coin is null) { throw new NotSupportedException("This is impossible."); } roundRegistered.CoinsRegistered.Add(coin); State.RemoveCoinFromWaitingList(coin); } roundRegistered.ActiveOutputAddress = activeAddress; roundRegistered.ChangeOutputAddress = changeAddress; roundRegistered.UnblindedSignature = unblindedSignature; roundRegistered.AliceClient = aliceClient; } } catch (Exception ex) { Logger.LogError <CcjClient>(ex); } }
public async Task SendTestsAsync() { (string password, IRPCClient rpc, Network network, _, ServiceConfiguration serviceConfiguration, BitcoinStore bitcoinStore, Backend.Global global) = await Common.InitializeTestEnvironmentAsync(RegTestFixture, 1); bitcoinStore.IndexStore.NewFilter += Common.Wallet_NewFilterProcessed; // Create the services. // 1. Create connection service. var nodes = new NodesGroup(global.Config.Network, requirements: Constants.NodeRequirements); nodes.ConnectedNodes.Add(await RegTestFixture.BackendRegTestNode.CreateNewP2pNodeAsync()); // 2. Create mempool service. Node node = await RegTestFixture.BackendRegTestNode.CreateNewP2pNodeAsync(); node.Behaviors.Add(bitcoinStore.CreateUntrustedP2pBehavior()); // 3. Create wasabi synchronizer service. var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null); // 4. Create key manager service. var keyManager = KeyManager.CreateNew(out _, password); // 5. Create wallet service. var workDir = Tests.Common.GetWorkDir(); CachedBlockProvider blockProvider = new CachedBlockProvider( new P2pBlockProvider(nodes, null, synchronizer, serviceConfiguration, network), bitcoinStore.BlockRepository); var walletManager = new WalletManager(network, new WalletDirectories(workDir)); walletManager.RegisterServices(bitcoinStore, synchronizer, nodes, serviceConfiguration, synchronizer, blockProvider); // Get some money, make it confirm. var key = keyManager.GetNextReceiveKey("foo label", out _); var key2 = keyManager.GetNextReceiveKey("foo label", out _); var txId = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m)); Assert.NotNull(txId); await rpc.GenerateAsync(1); var txId2 = await rpc.SendToAddressAsync(key2.GetP2wpkhAddress(network), Money.Coins(1m)); Assert.NotNull(txId2); await rpc.GenerateAsync(1); try { Interlocked.Exchange(ref Common.FiltersProcessedByWalletCount, 0); nodes.Connect(); // Start connection service. node.VersionHandshake(); // Start mempool service. synchronizer.Start(requestInterval: TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(5), 10000); // Start wasabi synchronizer service. // Wait until the filter our previous transaction is present. var blockCount = await rpc.GetBlockCountAsync(); await Common.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), blockCount); var wallet = await walletManager.AddAndStartWalletAsync(keyManager); var broadcaster = new TransactionBroadcaster(network, bitcoinStore, synchronizer, nodes, walletManager, rpc); var waitCount = 0; while (wallet.Coins.Sum(x => x.Amount) == Money.Zero) { await Task.Delay(1000); waitCount++; if (waitCount >= 21) { Logger.LogInfo("Funding transaction to the wallet did not arrive."); return; // Very rarely this test fails. I have no clue why. Probably because all these RegTests are interconnected, anyway let's not bother the CI with it. } } var scp = new Key().ScriptPubKey; var res2 = wallet.BuildTransaction(password, new PaymentIntent(scp, Money.Coins(0.05m), label: "foo"), FeeStrategy.CreateFromConfirmationTarget(5), allowUnconfirmed: false); Assert.NotNull(res2.Transaction); Assert.Single(res2.OuterWalletOutputs); Assert.Equal(scp, res2.OuterWalletOutputs.Single().ScriptPubKey); Assert.Single(res2.InnerWalletOutputs); Assert.True(res2.Fee > Money.Satoshis(2 * 100)); // since there is a sanity check of 2sat/vb in the server Assert.InRange(res2.FeePercentOfSent, 0, 1); Assert.Single(res2.SpentCoins); var spentCoin = Assert.Single(res2.SpentCoins); Assert.Contains(new[] { key.P2wpkhScript, key2.P2wpkhScript }, x => x == spentCoin.ScriptPubKey); Assert.Equal(Money.Coins(1m), res2.SpentCoins.Single().Amount); Assert.False(res2.SpendsUnconfirmed); await broadcaster.SendTransactionAsync(res2.Transaction); Assert.Contains(res2.InnerWalletOutputs.Single(), wallet.Coins); #region Basic Script receive = keyManager.GetNextReceiveKey("Basic", out _).P2wpkhScript; Money amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 2; var res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true); foreach (SmartCoin coin in res.SpentCoins) { Assert.False(coin.CoinJoinInProgress); Assert.True(coin.Confirmed); Assert.Null(coin.SpenderTransactionId); Assert.True(coin.Unspent); } Assert.Equal(2, res.InnerWalletOutputs.Count()); Assert.Empty(res.OuterWalletOutputs); var activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); var changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive); Assert.Equal(receive, activeOutput.ScriptPubKey); Assert.Equal(amountToSend, activeOutput.Amount); if (res.SpentCoins.Sum(x => x.Amount) - activeOutput.Amount == res.Fee) // this happens when change is too small { Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == activeOutput.Amount); Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}"); } Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"TxId: {res.Transaction.GetHash()}"); var foundReceive = false; Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2); foreach (var output in res.Transaction.Transaction.Outputs) { if (output.ScriptPubKey == receive) { foundReceive = true; Assert.Equal(amountToSend, output.Value); } } Assert.True(foundReceive); await broadcaster.SendTransactionAsync(res.Transaction); #endregion Basic #region SubtractFeeFromAmount receive = keyManager.GetNextReceiveKey("SubtractFeeFromAmount", out _).P2wpkhScript; amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3; res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, subtractFee: true, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Equal(2, res.InnerWalletOutputs.Count()); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive); Assert.Equal(receive, activeOutput.ScriptPubKey); Assert.Equal(amountToSend - res.Fee, activeOutput.Amount); Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount); Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"TxId: {res.Transaction.GetHash()}"); foundReceive = false; Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2); foreach (var output in res.Transaction.Transaction.Outputs) { if (output.ScriptPubKey == receive) { foundReceive = true; Assert.Equal(amountToSend - res.Fee, output.Value); } } Assert.True(foundReceive); #endregion SubtractFeeFromAmount #region LowFee res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Equal(2, res.InnerWalletOutputs.Count()); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive); Assert.Equal(receive, activeOutput.ScriptPubKey); Assert.Equal(amountToSend, activeOutput.Amount); Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount); Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"TxId: {res.Transaction.GetHash()}"); foundReceive = false; Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2); foreach (var output in res.Transaction.Transaction.Outputs) { if (output.ScriptPubKey == receive) { foundReceive = true; Assert.Equal(amountToSend, output.Value); } } Assert.True(foundReceive); #endregion LowFee #region MediumFee res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.OneDayConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Equal(2, res.InnerWalletOutputs.Count()); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive); Assert.Equal(receive, activeOutput.ScriptPubKey); Assert.Equal(amountToSend, activeOutput.Amount); Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount); Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"TxId: {res.Transaction.GetHash()}"); foundReceive = false; Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2); foreach (var output in res.Transaction.Transaction.Outputs) { if (output.ScriptPubKey == receive) { foundReceive = true; Assert.Equal(amountToSend, output.Value); } } Assert.True(foundReceive); #endregion MediumFee #region HighFee res = wallet.BuildTransaction(password, new PaymentIntent(receive, amountToSend, label: "foo"), FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Equal(2, res.InnerWalletOutputs.Count()); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); changeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey != receive); Assert.Equal(receive, activeOutput.ScriptPubKey); Assert.Equal(amountToSend, activeOutput.Amount); Assert.Contains(res.Transaction.Transaction.Outputs, x => x.Value == changeOutput.Amount); Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"Change Output: {changeOutput.Amount.ToString(false, true)} {changeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"TxId: {res.Transaction.GetHash()}"); foundReceive = false; Assert.InRange(res.Transaction.Transaction.Outputs.Count, 1, 2); foreach (var output in res.Transaction.Transaction.Outputs) { if (output.ScriptPubKey == receive) { foundReceive = true; Assert.Equal(amountToSend, output.Value); } } Assert.True(foundReceive); Assert.InRange(res.Fee, Money.Zero, res.Fee); Assert.InRange(res.Fee, res.Fee, res.Fee); await broadcaster.SendTransactionAsync(res.Transaction); #endregion HighFee #region MaxAmount receive = keyManager.GetNextReceiveKey("MaxAmount", out _).P2wpkhScript; res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Single(res.InnerWalletOutputs); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(); Assert.Equal(receive, activeOutput.ScriptPubKey); Assert.Single(res.Transaction.Transaction.Outputs); var maxBuiltTxOutput = res.Transaction.Transaction.Outputs.Single(); Assert.Equal(receive, maxBuiltTxOutput.ScriptPubKey); Assert.Equal(wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) - res.Fee, maxBuiltTxOutput.Value); await broadcaster.SendTransactionAsync(res.Transaction); #endregion MaxAmount #region InputSelection receive = keyManager.GetNextReceiveKey("InputSelection", out _).P2wpkhScript; var inputCountBefore = res.SpentCoins.Count(); res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true, allowedInputs: wallet.Coins.Where(x => !x.Unavailable).Select(x => x.OutPoint).Take(1)); Assert.Single(res.InnerWalletOutputs); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); Assert.True(inputCountBefore >= res.SpentCoins.Count()); Assert.Equal(res.SpentCoins.Count(), res.Transaction.Transaction.Inputs.Count); Assert.Equal(receive, activeOutput.ScriptPubKey); Logger.LogInfo($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogInfo($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogInfo($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogInfo($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogInfo($"TxId: {res.Transaction.GetHash()}"); Assert.Single(res.Transaction.Transaction.Outputs); res = wallet.BuildTransaction(password, new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "foo"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true, allowedInputs: new[] { res.SpentCoins.Select(x => x.OutPoint).First() }); Assert.Single(res.InnerWalletOutputs); Assert.Empty(res.OuterWalletOutputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); Assert.Single(res.Transaction.Transaction.Inputs); Assert.Single(res.Transaction.Transaction.Outputs); Assert.Single(res.SpentCoins); #endregion InputSelection #region Labeling Script receive2 = keyManager.GetNextReceiveKey("foo", out _).P2wpkhScript; res = wallet.BuildTransaction(password, new PaymentIntent(receive2, MoneyRequest.CreateAllRemaining(), "my label"), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Single(res.InnerWalletOutputs); Assert.Equal("foo, my label", res.InnerWalletOutputs.Single().Label); amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3; res = wallet.BuildTransaction( password, new PaymentIntent( new DestinationRequest(new Key(), amountToSend, label: "outgoing"), new DestinationRequest(new Key(), amountToSend, label: "outgoing2")), FeeStrategy.SevenDaysConfirmationTargetStrategy, allowUnconfirmed: true); Assert.Single(res.InnerWalletOutputs); Assert.Equal(2, res.OuterWalletOutputs.Count()); IEnumerable <string> change = res.InnerWalletOutputs.Single().Label.Labels; Assert.Contains("outgoing", change); Assert.Contains("outgoing2", change); await broadcaster.SendTransactionAsync(res.Transaction); IEnumerable <SmartCoin> unconfirmedCoins = wallet.Coins.Where(x => x.Height == Height.Mempool).ToArray(); IEnumerable <string> unconfirmedCoinLabels = unconfirmedCoins.SelectMany(x => x.Label.Labels).ToArray(); Assert.Contains("outgoing", unconfirmedCoinLabels); Assert.Contains("outgoing2", unconfirmedCoinLabels); IEnumerable <string> allKeyLabels = keyManager.GetKeys().SelectMany(x => x.Label.Labels); Assert.Contains("outgoing", allKeyLabels); Assert.Contains("outgoing2", allKeyLabels); Interlocked.Exchange(ref Common.FiltersProcessedByWalletCount, 0); await rpc.GenerateAsync(1); await Common.WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1); var bestHeight = new Height(bitcoinStore.SmartHeaderChain.TipHeight); IEnumerable <string> confirmedCoinLabels = wallet.Coins.Where(x => x.Height == bestHeight).SelectMany(x => x.Label.Labels); Assert.Contains("outgoing", confirmedCoinLabels); Assert.Contains("outgoing2", confirmedCoinLabels); allKeyLabels = keyManager.GetKeys().SelectMany(x => x.Label.Labels); Assert.Contains("outgoing", allKeyLabels); Assert.Contains("outgoing2", allKeyLabels); #endregion Labeling #region AllowedInputsDisallowUnconfirmed inputCountBefore = res.SpentCoins.Count(); receive = keyManager.GetNextReceiveKey("AllowedInputsDisallowUnconfirmed", out _).P2wpkhScript; var allowedInputs = wallet.Coins.Where(x => !x.Unavailable).Select(x => x.OutPoint).Take(1); var toSend = new PaymentIntent(receive, MoneyRequest.CreateAllRemaining(), "fizz"); // covers: // disallow unconfirmed with allowed inputs res = wallet.BuildTransaction(password, toSend, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, false, allowedInputs: allowedInputs); activeOutput = res.InnerWalletOutputs.Single(x => x.ScriptPubKey == receive); Assert.Single(res.InnerWalletOutputs); Assert.Empty(res.OuterWalletOutputs); Assert.Equal(receive, activeOutput.ScriptPubKey); Logger.LogDebug($"{nameof(res.Fee)}: {res.Fee}"); Logger.LogDebug($"{nameof(res.FeePercentOfSent)}: {res.FeePercentOfSent} %"); Logger.LogDebug($"{nameof(res.SpendsUnconfirmed)}: {res.SpendsUnconfirmed}"); Logger.LogDebug($"Active Output: {activeOutput.Amount.ToString(false, true)} {activeOutput.ScriptPubKey.GetDestinationAddress(network)}"); Logger.LogDebug($"TxId: {res.Transaction.GetHash()}"); Assert.True(inputCountBefore >= res.SpentCoins.Count()); Assert.False(res.SpendsUnconfirmed); Assert.Single(res.Transaction.Transaction.Inputs); Assert.Single(res.Transaction.Transaction.Outputs); Assert.Single(res.SpentCoins); Assert.True(inputCountBefore >= res.SpentCoins.Count()); Assert.Equal(res.SpentCoins.Count(), res.Transaction.Transaction.Inputs.Count); #endregion AllowedInputsDisallowUnconfirmed #region CustomChange // covers: // customchange // feePc > 1 var k1 = new Key(); var k2 = new Key(); res = wallet.BuildTransaction( password, new PaymentIntent( new DestinationRequest(k1, MoneyRequest.CreateChange()), new DestinationRequest(k2, Money.Coins(0.0003m), label: "outgoing")), FeeStrategy.TwentyMinutesConfirmationTargetStrategy); Assert.Contains(k1.ScriptPubKey, res.OuterWalletOutputs.Select(x => x.ScriptPubKey)); Assert.Contains(k2.ScriptPubKey, res.OuterWalletOutputs.Select(x => x.ScriptPubKey)); #endregion CustomChange #region FeePcHigh res = wallet.BuildTransaction( password, new PaymentIntent(new Key(), Money.Coins(0.0003m), label: "outgoing"), FeeStrategy.TwentyMinutesConfirmationTargetStrategy); Assert.True(res.FeePercentOfSent > 1); var newChangeK = keyManager.GenerateNewKey("foo", KeyState.Clean, isInternal: true); res = wallet.BuildTransaction( password, new PaymentIntent( new DestinationRequest(newChangeK.P2wpkhScript, MoneyRequest.CreateChange(), "boo"), new DestinationRequest(new Key(), Money.Coins(0.0003m), label: "outgoing")), FeeStrategy.TwentyMinutesConfirmationTargetStrategy); Assert.True(res.FeePercentOfSent > 1); Assert.Single(res.OuterWalletOutputs); Assert.Single(res.InnerWalletOutputs); SmartCoin changeRes = res.InnerWalletOutputs.Single(); Assert.Equal(newChangeK.P2wpkhScript, changeRes.ScriptPubKey); Assert.Equal(newChangeK.Label, changeRes.Label); Assert.Equal(KeyState.Clean, newChangeK.KeyState); // Still clean, because the tx wasn't yet propagated. #endregion FeePcHigh } finally { bitcoinStore.IndexStore.NewFilter -= Common.Wallet_NewFilterProcessed; await walletManager.RemoveAndStopAllAsync(CancellationToken.None); // Dispose wasabi synchronizer service. if (synchronizer is { })
public async Task DequeueCoinsFromMixAsync(SmartCoin coin, string reason) { await DequeueCoinsFromMixAsync(new[] { coin }, reason); }
private (HdPubKey change, IEnumerable <HdPubKey> active) GetOutputsToRegister(Money baseDenomination, int mixingLevelCount, IEnumerable <TxoRef> coinsToRegister) { // Figure out how many mixing level we need to register active outputs. Money inputSum = Money.Zero; foreach (TxoRef coinReference in coinsToRegister) { SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference); inputSum += coin.Amount; } int maximumMixingLevelCount = 1; var denominations = new List <Money> { baseDenomination }; for (int i = 1; i < mixingLevelCount; i++) { Money denom = denominations.Last() * 2; denominations.Add(denom); if (inputSum > denom) { maximumMixingLevelCount = i + 1; } } string changeLabel = "ZeroLink Change"; string activeLabel = "ZeroLink Mixed Coin"; var keysToSurelyRegister = ExposedLinks.Where(x => coinsToRegister.Contains(x.Key)).SelectMany(x => x.Value).Select(x => x.Key).ToArray(); var keysTryNotToRegister = ExposedLinks.SelectMany(x => x.Value).Select(x => x.Key).Except(keysToSurelyRegister).ToArray(); // Get all locked internal keys we have and assert we have enough. KeyManager.AssertLockedInternalKeysIndexed(howMany: maximumMixingLevelCount + 1); IEnumerable <HdPubKey> allLockedInternalKeys = KeyManager.GetKeys(x => x.IsInternal && x.KeyState == KeyState.Locked && !keysTryNotToRegister.Contains(x)); // If any of our inputs have exposed address relationship then prefer that. allLockedInternalKeys = keysToSurelyRegister.Concat(allLockedInternalKeys).Distinct(); // Prefer not to bloat the wallet: if (allLockedInternalKeys.Count() <= maximumMixingLevelCount) { allLockedInternalKeys = allLockedInternalKeys.Concat(keysTryNotToRegister).Distinct(); } var newKeys = new List <HdPubKey>(); for (int i = allLockedInternalKeys.Count(); i <= maximumMixingLevelCount + 1; i++) { HdPubKey k = KeyManager.GenerateNewKey("", KeyState.Locked, isInternal: true, toFile: false); newKeys.Add(k); } allLockedInternalKeys = allLockedInternalKeys.Concat(newKeys); // Select the change and active keys to register and label them accordingly. HdPubKey change = allLockedInternalKeys.First(); change.SetLabel(changeLabel); var actives = new List <HdPubKey>(); foreach (HdPubKey active in allLockedInternalKeys.Skip(1).Take(maximumMixingLevelCount)) { actives.Add(active); active.SetLabel(activeLabel); } // Remember which links we are exposing. var outLinks = new List <HdPubKeyBlindedPair> { new HdPubKeyBlindedPair(change, isBlinded: false) }; foreach (var active in actives) { outLinks.Add(new HdPubKeyBlindedPair(active, isBlinded: true)); } foreach (TxoRef coin in coinsToRegister) { if (!ExposedLinks.TryAdd(coin, outLinks)) { var newOutLinks = new List <HdPubKeyBlindedPair>(); foreach (HdPubKeyBlindedPair link in ExposedLinks[coin]) { newOutLinks.Add(link); } foreach (HdPubKeyBlindedPair link in outLinks) { HdPubKeyBlindedPair found = newOutLinks.FirstOrDefault(x => x == link); if (found == default) { newOutLinks.Add(link); } else // If already in it then update the blinded value if it's getting exposed just now. (eg. the change) { if (found.IsBlinded) { found.IsBlinded = link.IsBlinded; } } } ExposedLinks[coin] = newOutLinks; } } // Save our modifications in the keymanager before we give back the selected keys. KeyManager.ToFile(); return(change, actives); }