wabisabi fixes + aff

This commit is contained in:
Kukks
2023-02-09 15:43:55 +01:00
parent 4eda579661
commit 8937565a5d
26 changed files with 625 additions and 18 deletions

View File

@@ -0,0 +1,126 @@
using System;
using System.IO;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin.DataEncoders;
using WalletWasabi.Affiliation.Models;
using WalletWasabi.Affiliation.Models.CoinjoinRequest;
namespace BTCPayServer.Plugins.Wabisabi.AffiliateServer;
public class WabisabiAffiliateSettings
{
public bool Enabled { get; set; }
public string SigningKey { get; set; }
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Route("plugins/wabisabi-affiliate")]
public class AffiliateServerController:Controller
{
private readonly SettingsRepository _settingsRepository;
private readonly IOptions<DataDirectories> _dataDirectories;
private readonly ILogger<AffiliateServerController> _logger;
public AffiliateServerController(SettingsRepository settingsRepository, IOptions<DataDirectories> dataDirectories, ILogger<AffiliateServerController> logger)
{
_settingsRepository = settingsRepository;
_dataDirectories = dataDirectories;
_logger = logger;
}
[HttpGet("edit")]
public async Task<IActionResult> Edit()
{
var settings =
await _settingsRepository.GetSettingAsync<WabisabiAffiliateSettings>();
return View(settings);
}
[HttpPost("edit")]
public async Task<IActionResult> Edit(WabisabiAffiliateSettings settings)
{
await _settingsRepository.UpdateSetting(settings);
return RedirectToAction("Edit");
}
[HttpGet("history")]
public async Task<IActionResult> ViewRequests()
{
var path = Path.Combine(_dataDirectories.Value.DataDir, "Plugins", "CoinjoinAffiliate", "History.txt");
if (!System.IO.File.Exists(path))
return NotFound();
return File(path, MediaTypeNames.Text.Plain);
}
[AllowAnonymous]
[HttpPost("get_status")]
public async Task<IActionResult> GetStatus()
{
var settings =
await _settingsRepository.GetSettingAsync<WabisabiAffiliateSettings>();
if(settings?.Enabled is true&& !string.IsNullOrEmpty(settings.SigningKey))
return Ok(new { });
return NotFound();
}
private static ECDsa ecdsa = ECDsa.Create();
[AllowAnonymous]
[HttpPost("get_coinjoin_request")]
public async Task<IActionResult> GetCoinjoinRequest([FromBody] GetCoinjoinRequestRequest request)
{
var settings = await _settingsRepository.GetSettingAsync<WabisabiAffiliateSettings>();
if (settings?.Enabled is not true)
{
return NotFound();
}
var keyB = Encoders.Hex.DecodeData(settings.SigningKey);
ecdsa.ImportSubjectPublicKeyInfo(keyB.AsSpan(), out _);
Payload payload = new(new Header(), request.Body);
try
{
var valid = ecdsa.VerifyData(payload.GetCanonicalSerialization(), request.Signature, HashAlgorithmName.SHA256);
if(!valid)
return NotFound();
var path = Path.Combine(_dataDirectories.Value.DataDir, "Plugins", "CoinjoinAffiliate", "History.txt");
string rawBody;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
rawBody = (await reader.ReadToEndAsync());
}
await System.IO.File.AppendAllLinesAsync(path, new[] {rawBody.Replace(Environment.NewLine, "")}, Encoding.UTF8);
return Ok(new GetCoinjoinRequestResponse(Array.Empty<byte>()));
}
catch (Exception e)
{
_logger.LogError(e, "Failed on GetCoinjoinRequest" );
return NotFound();
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using NBitcoin;
using WalletWasabi.WabiSabi.Models;
namespace WalletWasabi.Affiliation.Models;
public record AffiliateInformation
(
ImmutableArray<AffiliationFlag> RunningAffiliateServers,
ImmutableDictionary<string, ImmutableDictionary<AffiliationFlag, byte[]>> CoinjoinRequests
)
{
public static readonly AffiliateInformation Empty = new(ImmutableArray<AffiliationFlag>.Empty, ImmutableDictionary<string, ImmutableDictionary<AffiliationFlag, byte[]>>.Empty);
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using WalletWasabi.Affiliation.Serialization;
using System.Text;
namespace WalletWasabi.Affiliation.Models.CoinjoinRequest;
public record Body
{
public Body(IEnumerable<Input> inputs, IEnumerable<Output> outputs, long slip44CoinType, decimal feeRate, long noFeeThreshold, long minRegistrableAmount, long timestamp)
{
Inputs = inputs;
Outputs = outputs;
Slip44CoinType = slip44CoinType;
FeeRate = feeRate;
NoFeeThreshold = noFeeThreshold;
MinRegistrableAmount = minRegistrableAmount;
Timestamp = timestamp;
}
[JsonProperty(PropertyName = "inputs")]
public IEnumerable<Input> Inputs { get; }
[JsonProperty(PropertyName = "outputs")]
public IEnumerable<Output> Outputs { get; }
[JsonProperty(PropertyName = "slip44_coin_type")]
public long Slip44CoinType { get; }
[JsonProperty(PropertyName = "fee_rate")]
[JsonConverter(typeof(AffiliationFeeRateJsonConverter))]
public decimal FeeRate { get; }
[JsonProperty(PropertyName = "no_fee_threshold")]
public long NoFeeThreshold { get; }
[JsonProperty(PropertyName = "min_registrable_amount")]
public long MinRegistrableAmount { get; }
[JsonProperty(PropertyName = "timestamp")]
public long Timestamp { get; }
}

View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace WalletWasabi.Affiliation.Models.CoinjoinRequest;
public record Header
{
[JsonProperty(PropertyName = "title")]
public static readonly string Title = "payment request";
[JsonProperty(PropertyName = "version")]
public static readonly int Version = 1;
}

View File

@@ -0,0 +1,62 @@
using System;
using NBitcoin;
using Newtonsoft.Json;
using WalletWasabi.Affiliation.Serialization;
using WalletWasabi.WabiSabi.Models;
namespace WalletWasabi.Affiliation.Models.CoinjoinRequest;
public record Input
{
public Input(Outpoint prevout, byte[] scriptPubkey, bool isAffiliated, bool isNoFee)
{
Prevout = prevout;
ScriptPubkey = scriptPubkey;
IsAffiliated = isAffiliated;
IsNoFee = isNoFee;
if (isNoFee && isAffiliated)
{
Logging.Logger.LogWarning($"Detected input with redundant affiliation flag: {Convert.ToHexString(prevout.Hash)}, {prevout.Index}");
}
}
[JsonProperty(PropertyName = "prevout")]
public Outpoint Prevout { get; }
[JsonProperty(PropertyName = "script_pubkey")]
[JsonConverter(typeof(AffiliationByteArrayJsonConverter))]
public byte[] ScriptPubkey { get; }
[JsonProperty(PropertyName = "is_affiliated")]
public bool IsAffiliated { get; }
[JsonProperty(PropertyName = "is_no_fee")]
public bool IsNoFee { get; }
public static Input FromAffiliateInput(AffiliateInput affiliateInput, AffiliationFlag affiliationFlag)
{
return new Input(Outpoint.FromOutPoint(affiliateInput.Prevout), affiliateInput.ScriptPubKey.ToBytes(), affiliateInput.AffiliationFlag == affiliationFlag, affiliateInput.IsNoFee);
}
}
public record AffiliateInput
{
public AffiliateInput(OutPoint prevout, Script scriptPubKey, AffiliationFlag affiliationFlag, bool isNoFee)
{
Prevout = prevout;
ScriptPubKey = scriptPubKey;
AffiliationFlag = affiliationFlag;
IsNoFee = isNoFee;
}
public AffiliateInput(Coin coin, AffiliationFlag affiliationFlag, bool isNoFee)
: this(coin.Outpoint, coin.ScriptPubKey, affiliationFlag, isNoFee)
{
}
public OutPoint Prevout { get; }
public Script ScriptPubKey { get; }
public AffiliationFlag AffiliationFlag { get; }
public bool IsNoFee { get; }
}

View File

@@ -0,0 +1,26 @@
using NBitcoin;
using Newtonsoft.Json;
using WalletWasabi.Affiliation.Serialization;
namespace WalletWasabi.Affiliation.Models.CoinjoinRequest;
public record Outpoint
{
public Outpoint(byte[] hash, long index)
{
Hash = hash;
Index = index;
}
[JsonProperty(PropertyName = "hash")]
[JsonConverter(typeof(AffiliationByteArrayJsonConverter))]
public byte[] Hash { get; }
[JsonProperty(PropertyName = "index")]
public long Index { get; }
public static Outpoint FromOutPoint(OutPoint outPoint)
{
return new Outpoint(outPoint.Hash.ToBytes(lendian: true), outPoint.N);
}
}

View File

@@ -0,0 +1,26 @@
using NBitcoin;
using Newtonsoft.Json;
using WalletWasabi.Affiliation.Serialization;
namespace WalletWasabi.Affiliation.Models.CoinjoinRequest;
public record Output
{
public Output(long amount, byte[] script_pubkey)
{
Amount = amount;
ScriptPubkey = script_pubkey;
}
[JsonProperty(PropertyName = "amount")]
public long Amount { get; }
[JsonProperty(PropertyName = "script_pubkey")]
[JsonConverter(typeof(AffiliationByteArrayJsonConverter))]
public byte[] ScriptPubkey { get; }
public static Output FromTxOut(TxOut txOut)
{
return new Output(txOut.Value, txOut.ScriptPubKey.ToBytes());
}
}

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
using WalletWasabi.Affiliation.Serialization;
using System.Text;
namespace WalletWasabi.Affiliation.Models.CoinjoinRequest;
public record Payload
{
public Payload(Header header, Body body)
{
Header = header;
Body = body;
}
[JsonProperty(PropertyName = "header")]
public Header Header { get; }
[JsonProperty(PropertyName = "body")]
public Body Body { get; }
public byte[] GetCanonicalSerialization()
{
return Encoding.ASCII.GetBytes(JsonConvert.SerializeObject(this, CanonicalJsonSerializationOptions.Settings));
}
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
using WalletWasabi.Affiliation.Serialization;
using WalletWasabi.Affiliation.Models.CoinjoinRequest;
namespace WalletWasabi.Affiliation.Models;
public record GetCoinjoinRequestRequest
{
public GetCoinjoinRequestRequest(Body body, byte[] signature)
{
Body = body;
Signature = signature;
}
[JsonProperty(PropertyName = "signature")]
[JsonConverter(typeof(AffiliationByteArrayJsonConverter))]
public byte[] Signature { get; }
[JsonProperty(PropertyName = "body")]
public Body Body { get; }
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace WalletWasabi.Affiliation.Models;
public class GetCoinjoinRequestResponse
{
[JsonProperty(PropertyName = "coinjoin_request")]
public byte[] CoinjoinRequest;
public GetCoinjoinRequestResponse(byte[] coinjoinRequest)
{
CoinjoinRequest = coinjoinRequest;
}
}

View File

@@ -0,0 +1,3 @@
namespace WalletWasabi.Affiliation.Models;
public record StatusRequest();

View File

@@ -0,0 +1,3 @@
namespace WalletWasabi.Affiliation.Models;
public record StatusResponse();

View File

@@ -0,0 +1,24 @@
using System;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using WalletWasabi.Helpers;
namespace WalletWasabi.Affiliation.Serialization;
public class AffiliationByteArrayJsonConverter : JsonConverter<byte[]>
{
public override byte[]? ReadJson(JsonReader reader, Type objectType, byte[]? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is string serialized)
{
return Convert.FromHexString(serialized);
}
throw new JsonSerializationException("Cannot deserialize object.");
}
public override void WriteJson(JsonWriter writer, byte[]? value, JsonSerializer serializer)
{
Guard.NotNull(nameof(value), value);
writer.WriteValue(Convert.ToHexString(value).ToLower());
}
}

View File

@@ -0,0 +1,42 @@
using System;
using Newtonsoft.Json;
namespace WalletWasabi.Affiliation.Serialization;
public class AffiliationFeeRateJsonConverter : JsonConverter<decimal>
{
public static readonly decimal Base = 1e-8m;
private static long EncodeDecimal(decimal value)
{
return (long)decimal.Round(value / Base);
}
private static decimal DecodeDecimal(long value)
{
return value * Base;
}
private static bool IsEncodable(decimal value)
{
return DecodeDecimal(EncodeDecimal(value)) == value;
}
public override decimal ReadJson(JsonReader reader, Type objectType, decimal existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is long number)
{
return DecodeDecimal(number);
}
throw new JsonSerializationException("Cannot deserialize object.");
}
public override void WriteJson(JsonWriter writer, decimal value, JsonSerializer serializer)
{
if (!IsEncodable(value))
{
throw new ArgumentException("Decimal cannot be unambiguously encodable.", nameof(value));
}
writer.WriteValue(EncodeDecimal(value));
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.ComponentModel;
using System.Globalization;
using WalletWasabi.WabiSabi.Models;
namespace WalletWasabi.Affiliation.Serialization;
public class AffiliationFlagConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string)
{
return new AffiliationFlag((string)value);
}
throw new NotSupportedException();
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string))
{
if (value is AffiliationFlag)
{
return ((AffiliationFlag)value).Name;
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace WalletWasabi.Affiliation.Serialization;
public static class AffiliationJsonSerializationOptions
{
public static readonly List<JsonConverter> Converters = new()
{
new AffiliationByteArrayJsonConverter(),
new AffiliationFeeRateJsonConverter()
};
public static readonly JsonSerializerSettings Settings = new() { Converters = Converters };
}

View File

@@ -0,0 +1,56 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Collections.Generic;
using System.Linq;
namespace WalletWasabi.Affiliation.Serialization;
public static class CanonicalJsonSerializationOptions
{
/// <summary>
/// JSON settings that enforces JSON's objects' properties' to be serialized in alphabetical order.
/// </summary>
public static readonly JsonSerializerSettings Settings = new()
{
ContractResolver = new OrderedContractResolver(),
Converters = AffiliationJsonSerializationOptions.Converters,
// Intentionally enforced default value.
Formatting = Formatting.None
};
/// <seealso href="https://stackoverflow.com/a/11309106/3744182"/>
private class OrderedContractResolver : DefaultContractResolver
{
private static bool IsValidCharacter(char c)
{
return char.IsAscii(c) && ((char.IsLetter(c) && char.IsLower(c)) || char.IsDigit(c) || c == '_');
}
private static bool IsValidPropertyName(string name)
{
return name.All(IsValidCharacter);
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IEnumerable<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
foreach (JsonProperty property in properties)
{
if (property.PropertyName is null)
{
throw new JsonSerializationException("Property name is not set");
}
if (!IsValidPropertyName(property.PropertyName))
{
throw new JsonSerializationException("Object property contains an invalid character.");
}
}
return properties.OrderBy(p => p.PropertyName, StringComparer.Ordinal).ToList();
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Immutable;
using System.ComponentModel;
using WalletWasabi.WabiSabi.Models;
namespace WalletWasabi.Affiliation.Serialization;
public class DefaultAffiliateServersAttribute : DefaultValueAttribute
{
public DefaultAffiliateServersAttribute() : base(ImmutableDictionary<AffiliationFlag, string>.Empty)
{
}
}

View File

@@ -0,0 +1,11 @@
using System.ComponentModel;
using WalletWasabi.WabiSabi.Models;
namespace WalletWasabi.Affiliation.Serialization;
public class DefaultAffiliationFlagAttribute : DefaultValueAttribute
{
public DefaultAffiliationFlagAttribute() : base(AffiliationFlag.Default)
{
}
}

View File

@@ -46,7 +46,12 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
}) })
.Where(coin => .Where(coin =>
{ {
if (!_wallet.WabisabiStoreSettings.PlebMode &&
_wallet.WabisabiStoreSettings.CrossMixBetweenCoordinatorsMode ==
WabisabiStoreSettings.CrossMixMode.Always)
{
return true;
}
if (!coin.HdPubKey.Label.Contains("coinjoin") || coin.HdPubKey.Label.Contains(utxoSelectionParameters.CoordinatorName)) if (!coin.HdPubKey.Label.Contains("coinjoin") || coin.HdPubKey.Label.Contains(utxoSelectionParameters.CoordinatorName))
{ {
return true; return true;

View File

@@ -150,7 +150,7 @@ public class WabisabiCoordinatorService : PeriodicRunner
cacheKey, cacheKey,
action: async (_, cancellationToken) => action: async (_, cancellationToken) =>
{ {
var result = await _explorerClient.GetFeeRateAsync(confirmationTarget, cancellationToken); var result = await _explorerClient.GetFeeRateAsync(confirmationTarget, new FeeRate(100m), cancellationToken);
return new EstimateSmartFeeResponse() {FeeRate = result.FeeRate, Blocks = result.BlockCount}; return new EstimateSmartFeeResponse() {FeeRate = result.FeeRate, Blocks = result.BlockCount};
}, },
options: CacheOptionsWithExpirationToken(size: 1, expireInSeconds: 60), options: CacheOptionsWithExpirationToken(size: 1, expireInSeconds: 60),

View File

@@ -1,2 +1,2 @@
dotnet publish -c Altcoins-Release -o bin/Altcoins-Debug/net6.0 dotnet publish BTCPayServer.Plugins.Wabisabi.csproj -c Release -o bin/publish/BTCPayServer.Plugins.Wabisabi
dotnet run -p ../../submodules/btcpayserver/BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.Wabisabi BTCPayServer.Plugins.Wabisabi ../packed dotnet run -p ../../submodules/btcpayserver/BTCPayServer.PluginPacker bin/publish/BTCPayServer.Plugins.Wabisabi BTCPayServer.Plugins.Wabisabi ../packed

View File

@@ -0,0 +1,34 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Plugins.Wabisabi.AffiliateServer.WabisabiAffiliateSettings
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["NavPartialName"] = "../UIServer/_Nav";
}
<h2 class="mb-4">Coinjoin affiliate configuration</h2>
<form method="post">
<div class="row">
<div class="col-xxl-constrain col-xl-8">
<div class="form-group form-check">
<label asp-for="Enabled" class="form-check-label">Enable </label>
<input asp-for="Enabled" type="checkbox" class="form-check-input"/>
</div>
<div class="form-group pt-3">
<label class="form-label" for="config">Coordinator Key</label>
<input type="text" class="form-control " asp-for="SigningKey">
</input>
</div>
</div>
</div>
<button name="command" type="submit" value="save" class="btn btn-primary mt-2">Save</button>
</form>
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
}

View File

@@ -41,7 +41,7 @@
} }
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover w-100">
<thead> <thead>
<tr> <tr>
<th class="w-125px">Round</th> <th class="w-125px">Round</th>

View File

@@ -63,9 +63,7 @@
} }
else else
{ {
<div class="widget store-wallet-balance " style=" <div class="widget store-wallet-balance" >
max-height: 500px;
overflow-y: auto;">
<header> <header>
<h3>Recent Coinjoins</h3> <h3>Recent Coinjoins</h3>
@if (cjHistory.Any()) @if (cjHistory.Any())
@@ -97,9 +95,7 @@
var privacyPercentage = Math.Round(privacy * 100); var privacyPercentage = Math.Round(privacy * 100);
var colorCoins = coins.GroupBy(coin => coin.CoinColor(wallet.AnonymitySetTarget)).ToDictionary(grouping => grouping.Key, grouping => grouping); var colorCoins = coins.GroupBy(coin => coin.CoinColor(wallet.AnonymitySetTarget)).ToDictionary(grouping => grouping.Key, grouping => grouping);
<div class="widget store-numbers" style=" <div class="widget store-numbers" >
max-height: 500px;
overflow-y: auto;">
@if (wallet is BTCPayWallet btcPayWallet) @if (wallet is BTCPayWallet btcPayWallet)
{ {

View File

@@ -163,14 +163,10 @@ namespace BTCPayServer.Plugins.Wabisabi
return View(vm); return View(vm);
case "save": case "save":
foreach (WabisabiStoreCoordinatorSettings settings in vm.Settings)
{
vm.InputLabelsAllowed = vm.InputLabelsAllowed.Where(s => !string.IsNullOrEmpty(s)).Distinct() vm.InputLabelsAllowed = vm.InputLabelsAllowed.Where(s => !string.IsNullOrEmpty(s)).Distinct()
.ToList(); .ToList();
vm.InputLabelsExcluded = vm.InputLabelsExcluded.Where(s => !string.IsNullOrEmpty(s)).Distinct() vm.InputLabelsExcluded = vm.InputLabelsExcluded.Where(s => !string.IsNullOrEmpty(s)).Distinct()
.ToList(); .ToList();
}
await _WabisabiService.SetWabisabiForStore(storeId, vm); await _WabisabiService.SetWabisabiForStore(storeId, vm);
TempData["SuccessMessage"] = "Wabisabi settings modified"; TempData["SuccessMessage"] = "Wabisabi settings modified";
return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId}); return RedirectToAction(nameof(UpdateWabisabiStoreSettings), new {storeId});