improve and add docs

This commit is contained in:
Kukks
2023-02-02 14:26:00 +01:00
parent c68342e449
commit 9786a03de3
21 changed files with 146 additions and 47 deletions

View File

@@ -43,6 +43,24 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
var percentageLeft = (effV.ToDecimal(MoneyUnit.BTC) / coin.Amount.ToDecimal(MoneyUnit.BTC));
// filter out low value coins where 50% of the value would be eaten up by fees
return effV > 0 && percentageLeft >= 0.5m;
})
.Where(coin =>
{
if (!coin.HdPubKey.Label.Contains("coinjoin") || coin.HdPubKey.Label.Contains(utxoSelectionParameters.CoordinatorName))
{
return true;
}
if (_wallet.WabisabiStoreSettings.PlebMode ||
_wallet.WabisabiStoreSettings.CrossMixBetweenCoordinatorsMode ==
WabisabiStoreSettings.CrossMixMode.WhenFree)
{
return coin.Amount <= utxoSelectionParameters.CoordinationFeeRate.PlebsDontPayThreshold;
}
return false;
});
var payments =
_wallet.BatchPayments
@@ -127,7 +145,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
// {
//still good to have a chance to proceed with a join to reduce timing analysis
var rand = Random.Shared.Next(1, 101);
var rand = Random.Shared.Next(1, 1001);
if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability)
{
_logger.LogInformation($"All coins are private and we have no pending payments. Skipping join.");

View File

@@ -36,7 +36,7 @@ using WalletWasabi.Wallets;
namespace BTCPayServer.Plugins.Wabisabi;
public class BTCPayWallet : IWallet, IDestinationProvider
public class BTCPayWallet : IWallet, IDestinationProvider
{
private readonly WalletRepository _walletRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
@@ -53,25 +53,24 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public readonly ILogger Logger;
public static readonly BlockchainAnalyzer BlockchainAnalyzer = new();
public BTCPayWallet(
WalletRepository walletRepository,
public BTCPayWallet(WalletRepository walletRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
BitcoinLikePayoutHandler bitcoinLikePayoutHandler,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
Services.Wallets.BTCPayWallet btcPayWallet,
PullPaymentHostedService pullPaymentHostedService,
OnChainPaymentMethodData onChainPaymentMethodData,
OnChainPaymentMethodData onChainPaymentMethodData,
DerivationStrategyBase derivationScheme,
ExplorerClient explorerClient,
ExplorerClient explorerClient,
BTCPayKeyChain keyChain,
IBTCPayServerClientFactory btcPayServerClientFactory,
IBTCPayServerClientFactory btcPayServerClientFactory,
string storeId,
WabisabiStoreSettings wabisabiStoreSettings,
WabisabiStoreSettings wabisabiStoreSettings,
IUTXOLocker utxoLocker,
ILoggerFactory loggerFactory,
ILoggerFactory loggerFactory,
Smartifier smartifier,
StoreRepository storeRepository,
ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins)
StoreRepository storeRepository,
ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> bannedCoins, EventAggregator eventAggregator)
{
KeyChain = keyChain;
_walletRepository = walletRepository;
@@ -90,7 +89,18 @@ public class BTCPayWallet : IWallet, IDestinationProvider
_smartifier = smartifier;
_storeRepository = storeRepository;
_bannedCoins = bannedCoins;
_eventAggregator = eventAggregator;
Logger = loggerFactory.CreateLogger($"BTCPayWallet_{storeId}");
_eventAggregator.SubscribeAsync<NewTransactionEvent>(async evt =>
{
if (evt.DerivationStrategy != DerivationScheme)
{
return;
}
_smartifier.OnNewTransaction(evt.TransactionData.TransactionHash, evt);
});
}
public string StoreId { get; set; }
@@ -153,6 +163,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public readonly Smartifier _smartifier;
private readonly StoreRepository _storeRepository;
private readonly ConcurrentDictionary<string, Dictionary<OutPoint, DateTimeOffset>> _bannedCoins;
private readonly EventAggregator _eventAggregator;
public IRoundCoinSelector GetCoinSelector()
{
@@ -199,13 +210,14 @@ public class BTCPayWallet : IWallet, IDestinationProvider
}
}
if (WabisabiStoreSettings.PlebMode || !WabisabiStoreSettings.CrossMixBetweenCoordinators)
if (WabisabiStoreSettings.PlebMode || WabisabiStoreSettings.CrossMixBetweenCoordinatorsMode != WabisabiStoreSettings.CrossMixMode.Always)
{
utxos = utxos.Where(data =>
!utxoLabels.TryGetValue(data.OutPoint, out var opLabels) ||
opLabels.coinjoinData is null ||
opLabels.coinjoinData.CoordinatorName == coordinatorName
)
opLabels.coinjoinData.CoordinatorName == coordinatorName ||
//the next criteria is handled in our coin selector as we dnt yet have access to round parameters
(WabisabiStoreSettings.CrossMixBetweenCoordinatorsMode == WabisabiStoreSettings.CrossMixMode.WhenFree))
.ToArray();
}
@@ -327,6 +339,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
public async Task RegisterCoinjoinTransaction(SuccessfulCoinJoinResult result, string coordinatorName)
{
await _savingProgress;
_savingProgress = RegisterCoinjoinTransactionInternal(result, coordinatorName);
await _savingProgress;
}
@@ -487,7 +500,7 @@ public class BTCPayWallet : IWallet, IDestinationProvider
}))}, "utxo");
}
_smartifier.Transactions.AddOrReplace(txHash, Task.FromResult(smartTx));
//
// var kp = await ExplorerClient.GetMetadataAsync<RootedKeyPath>(DerivationScheme,
// WellknownMetadataKeys.AccountKeyPath);

View File

@@ -42,7 +42,7 @@ public class Smartifier
WellknownMetadataKeys.AccountKeyPath);
}
private ConcurrentDictionary<uint256, Task<TransactionInformation>> cached = new();
public readonly ConcurrentDictionary<uint256, Task<TransactionInformation>> CachedTransactions = new();
public readonly ConcurrentDictionary<uint256, Task<SmartTransaction>> Transactions = new();
public readonly ConcurrentDictionary<OutPoint, Task<SmartCoin>> Coins = new();
private readonly Task<RootedKeyPath> _accountKeyPath;
@@ -58,14 +58,15 @@ public class Smartifier
var txs = coins.Select(data => data.OutPoint.Hash).Distinct();
foreach (uint256 tx in txs)
{
cached.TryAdd(tx, _explorerClient.GetTransactionAsync(DerivationScheme, tx));
if(!CachedTransactions.ContainsKey(tx))
CachedTransactions.TryAdd(tx, _explorerClient.GetTransactionAsync(DerivationScheme, tx));
}
foreach (var coin in coins)
{
var tx = await Transactions.GetOrAdd(coin.OutPoint.Hash, async uint256 =>
{
var unsmartTx = await cached[coin.OutPoint.Hash];
var unsmartTx = await CachedTransactions[coin.OutPoint.Hash];
if (unsmartTx is null)
{
return null;
@@ -85,7 +86,7 @@ public class Smartifier
potentialMatches.TryAdd(matchedInput, potentialMatchesForInput.ToArray());
foreach (IndexedTxIn potentialMatchForInput in potentialMatchesForInput)
{
var ti = await cached.GetOrAdd(potentialMatchForInput.PrevOut.Hash,
var ti = await CachedTransactions.GetOrAdd(potentialMatchForInput.PrevOut.Hash,
_explorerClient.GetTransactionAsync(DerivationScheme,
potentialMatchForInput.PrevOut.Hash));
@@ -147,7 +148,7 @@ public class Smartifier
var smartCoin = await Coins.GetOrAdd(coin.OutPoint, async point =>
{
utxoLabels.TryGetValue(coin.OutPoint, out var labels);
var unsmartTx = await cached[coin.OutPoint.Hash];
var unsmartTx = await CachedTransactions[coin.OutPoint.Hash];
var pubKey = DerivationScheme.GetChild(coin.KeyPath).GetExtPubKeys().First().PubKey;
var kp = (await _accountKeyPath).Derive(coin.KeyPath).KeyPath;
@@ -159,6 +160,11 @@ public class Smartifier
c.PropertyChanged += CoinPropertyChanged;
return c;
});
utxoLabels.TryGetValue(coin.OutPoint, out var labels);
smartCoin.HdPubKey.SetLabel(new SmartLabel(labels.labels ?? new HashSet<string>()));
smartCoin.HdPubKey.SetKeyState(current == 1 ? KeyState.Clean : KeyState.Used);
smartCoin.HdPubKey.SetAnonymitySet(labels.anonset);
tx.TryAddWalletOutput(smartCoin);
}
@@ -184,4 +190,5 @@ public class Smartifier
}
}
}
}

View File

@@ -97,7 +97,9 @@
var privacyPercentage = Math.Round(privacy * 100);
var colorCoins = coins.GroupBy(coin => coin.CoinColor(wallet.AnonymitySetTarget)).ToDictionary(grouping => grouping.Key, grouping => grouping);
<div class="widget store-numbers">
<div class="widget store-numbers" style="
max-height: 500px;
overflow-y: auto;">
@if (wallet is BTCPayWallet btcPayWallet)
{
@@ -300,17 +302,17 @@
<div class="list-group-item">
<h6>@coordinator.CoordinatorDisplayName</h6>
<div class="row ">
<span class="text-muted col-sm-12 col-md-9 p-0 text-break">
<span class="text-muted col-sm-12 col-xxl-9 p-0 text-break">
@coordinator.Coordinator
</span>
@if (!coordinator.WasabiCoordinatorStatusFetcher.Connected)
{
<p class="text-danger mb-0 col-sm-12 col-md-3 p-0 text-break">Not connected</p>
<p class="text-danger mb-0 col-sm-12 col-xxl-3 p-0 text-break">Not connected</p>
}
else
{
<p class="text-success mb-0 col-sm-12 col-md-3 p-0 text-break">Connected</p>
<p class="text-success mb-0 col-sm-12 col-xxl-3 p-0 text-break">Connected</p>
}
</div>
@{

View File

@@ -138,15 +138,19 @@
<input asp-for="BatchPayments" type="checkbox" class="form-check-input" />
<p class="text-muted">Batch your pending payments (on-chain payouts awaiting payment) inside coinjoins.</p>
</div>
<div class="form-group form-check">
<label asp-for="CrossMixBetweenCoordinators" class="form-check-label">Mix funds between different coordinators</label>
<input asp-for="CrossMixBetweenCoordinators" type="checkbox" class="form-check-input" />
<div class="form-group">
<label asp-for="CrossMixBetweenCoordinatorsMode" class="form-label">Mix funds between different coordinators</label>
<select asp-for="CrossMixBetweenCoordinatorsMode" class="form-select">
<option value="@WabisabiStoreSettings.CrossMixMode.WhenFree">Cross mix when free</option>
<option value="@WabisabiStoreSettings.CrossMixMode.Always">Always cross mix</option>
<option value="@WabisabiStoreSettings.CrossMixMode.Never">Never cross mix</option>
</select>
<p class="text-muted">Whether to allow mixed coins to be mixed within different coordinators for greater privacy (Warning: This will make your coins to lose the free remix within the same coordinator)</p>
</div>
<div class="form-group">
<label asp-for="ExtraJoinProbability" class="form-label">ExtraJoin Probability</label>
<input asp-for="ExtraJoinProbability" type="number" min="0" max="100" class="form-control" />
<p class="text-muted">Percentage probability of joining a round even if you have no payments to batch and all coins are private (Warning: a high probability will quickly eat up your balance in mining fees, suggestion:1-3%) </p>
<label asp-for="ExtraJoinProbability" class="form-label">Continuous Coinjoin</label>
<input asp-for="ExtraJoinProbability" type="number" min="0" max="100" step="any" class="form-control" />
<p class="text-muted">Percentage (100 = 1% reality) probability of joining a round even if you have no payments to batch and all coins are private, prevents timing analysis. (Warning: a high probability will quickly eat up your balance in mining fees) </p>
</div>
<div class="form-group ">
<label asp-for="MixToOtherWallet" class="form-check-label">Send to other wallet</label>
@@ -251,12 +255,10 @@
<tr>
<th scope="row">Fee charged</th>
<td>@(round.CoordinationFeeRate.Rate * 100)%
@if (round.CoordinationFeeRate.PlebsDontPayThreshold > 0)
{
@Safe.Raw($"+ Free under {round.CoordinationFeeRate.PlebsDontPayThreshold.ToDecimal(MoneyUnit.BTC)} BTC")
}
+ Free remixing
@{
var fee = $"{round.CoordinationFeeRate.Rate * 100}% + Free remixing {(round.CoordinationFeeRate.PlebsDontPayThreshold <= 0? string.Empty : $"+ Free under {round.CoordinationFeeRate.PlebsDontPayThreshold.ToDecimal(MoneyUnit.BTC)} BTC")}";
}
<td>@(fee)
</td>
</tr>
<tr>

View File

@@ -20,7 +20,6 @@ public class WabisabiStoreSettings
public bool BatchPayments { get; set; } = true;
public int ExtraJoinProbability { get; set; } = 0;
public bool CrossMixBetweenCoordinators { get; set; } = false;
public CrossMixMode CrossMixBetweenCoordinatorsMode { get; set; } = CrossMixMode.WhenFree;
public enum CrossMixMode

View File

@@ -99,8 +99,7 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
var keychain = new BTCPayKeyChain(explorerClient, derivationStrategy, masterKey, accountKey);
var smartifier = new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, _btcPayServerClientFactory, name,
CoinOnPropertyChanged);
var smartifier = new Smartifier(_serviceProvider.GetRequiredService<WalletRepository>(),explorerClient, derivationStrategy, name, UtxoLocker);
return (IWallet)new BTCPayWallet(
_serviceProvider.GetRequiredService<WalletRepository>(),
@@ -112,7 +111,8 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
pm, derivationStrategy, explorerClient, keychain,
_btcPayServerClientFactory, name, wabisabiStoreSettings, UtxoLocker,
_loggerFactory, smartifier,
_serviceProvider.GetRequiredService<StoreRepository>(), BannedCoins);
_serviceProvider.GetRequiredService<StoreRepository>(), BannedCoins,
_eventAggregator);
});
@@ -276,15 +276,9 @@ public class WalletProvider : PeriodicRunner,IWalletProvider
}, cancellationToken);
_subscription = _eventAggregator.SubscribeAsync<WalletChangedEvent>(@event =>
Check(@event.WalletId.StoreId, cancellationToken));
_subscription2 = _eventAggregator.SubscribeAsync<NewTransactionEvent>(HandleNewTransaction);
return base.StartAsync(cancellationToken);
}
private Task HandleNewTransaction(NewTransactionEvent arg)
{
throw new NotImplementedException();
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_subscription?.Dispose();

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,64 @@
# The BTCPay Server Coinjoin plugin
This plugin allows every BTCPay Server instance to integrate with the Wabisabi coinjoin protocol developed by [zkSNACKS](https://zksnacks.com/) ([Wasabi Wallet](https://wasabiwallet.io/)).
## Installation
First ensure that your BTCPay Server instance is at least version 1.8.0 and that NBXplorer is at least 2.3.58. If you are using the recommended Docker deployment method, it is as simple as [one-click](https://docs.btcpayserver.org/FAQ/ServerSettings/#how-to-update-btcpay-server).
Then, you will need to log in as an admin, click on "Manage plugins" in the side navigation, and click on "Install" on the "Coinjoin" plugin in the list. BTCPay Server will then ask you to restart in order to load the plugin.
After the restart, there should be a new navigation item in the side navigation, "Coinjoin", and the dashboard should have additional elements related to coinjoins.
![img_1.png](img_1.png)
## Usage
Your store needs to have a Bitcoin wallet configured and it needs to be set up as a [hot wallet](https://docs.btcpayserver.org/CreateWallet/#hot-wallet). Only native segwit (and potentially taproot) wallets will be able to join coinjoin rounds.
The easiest way to get started is to click on "Coinjoin" in the side navigation, choose the default "zkSNACKS" coordinator and click "save". BTCPay Server will automatically join coinjoin rounds and progress to enhancing the privacy of your wallet.
![img_2.png](img_2.png)
Coinjoin transactions will appear in the transactions list in your wallet as they happen, and will have at least 2 labels, "coinjoin", and the name of the coordinator.
![img_3.png](img_3.png)
## Spending privately
Coins which have gained some level of privacy will have an "anonset" label when using the BTCPay wallet coin selection feature. If you hover over the label, it will tell you the score it has gained.
![coinselection.png](coinselection.png)
It is up to you to use the coin selection feature correctly to make the best of your earned privacy on your coins.
Ideally you:
* select the least amount of coins possible
* select the highest level of privacy coins
* ideally use coins from different transactions
* spend entire coins to prevent change
We realize this is a complex selection and are working on an easier UI to help with this. As an initial experiment, we have added an action to let us attempt to select coins based on your sending amounts.
![img.png](img.png)
But the best way to spend privately is to use our unique **payment batching feature**, by utilizing BTCPay Server's [Payout](https://docs.btcpayserver.org/Payouts/) system. Simply set the destination and amount and click on "Schedule transaction", and the payment will be embedded directly inside the next coinjoin that can fulfill it.
![img_4.png](img_4.png)
## Additional Coordinators
We realize that the weakest link in these coinjoin protocols is the centralized coordinator aspect, and so have opted to support multiple coordinators, in parallel, from the get-go. You can discover additional coordinators over Nostr.
![img_5.png](img_5.png)
Please be cautious as some coordinators may be malicious in nature. Once a coordinator has been added and a coinjoin round has been discovered, you can click on "Coordinator Config" to see what their fees and round requirements are set to.
![img_6.png](img_6.png)
Ideally, the minimum number of inputs is 50 and the fee is below 1% (the default is 0.3%).
## Running a coordinator
In the spirit of "be the change you want to see in the world", this plugin ships with the ability to run your own coordinator (and publish it over Nostr for discoverability). This feature is still considered experimental, and may have [legal repercussions for operating a coordinator](https://bitcoinmagazine.com/technical/is-bitcoin-next-after-tornado-cash).
![img_7.png](img_7.png)
![img_8.png](img_8.png)
By default, the coordinator is configured to donate its generated fees to the [human rights foundation](https://hrf.org/), and [opensats](https://opensats.org/).
One enabled, the local coordinator appears in the coinjoin configuration of your store, and, if you configures the nostr settings, published to a relay so that others may discover the coordinator.
![img_9.png](img_9.png)