From 8cb34de43aeb0adbbff4c53683a6b282dd889805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:21:58 +0200 Subject: [PATCH] Added fee rate checks (#190) feat(Constants.cs): add MIN_SAT_PER_VB_RATIO, MAX_SAT_PER_VB_RATIO, and MAX_TX_FEE_RATIO for fee rate validation feat(LightningService.cs): implement fee rate validation for finalized PSBT using new constants to ensure fees are within acceptable range --- src/Helpers/Constants.cs | 16 +++++++++++++ src/Services/LightningService.cs | 39 ++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/Helpers/Constants.cs b/src/Helpers/Constants.cs index 527edaf9..73fd66b4 100644 --- a/src/Helpers/Constants.cs +++ b/src/Helpers/Constants.cs @@ -70,6 +70,14 @@ public class Constants public static readonly long ANCHOR_CLOSINGS_MINIMUM_SATS; public static readonly string DEFAULT_DERIVATION_PATH = "48'/1'"; public static readonly int SESSION_TIMEOUT_MILLISECONDS = 3_600_000; + + //Sat/vb ratio + public static decimal MIN_SAT_PER_VB_RATIO = 0.9m; + public static decimal MAX_SAT_PER_VB_RATIO = 2.0m; + /// + /// Max ratio of the tx total input sum that could be used as fee + /// + public static decimal MAX_TX_FEE_RATIO =0.5m; private static string? GetEnvironmentalVariableOrThrowIfNotTesting(string envVariableName, string? errorMessage = null) { @@ -190,6 +198,14 @@ static Constants() var timeout = Environment.GetEnvironmentVariable("SESSION_TIMEOUT_MILLISECONDS"); if (timeout != null) SESSION_TIMEOUT_MILLISECONDS = int.Parse(timeout); + + //Sat/vb ratio + var minSatPerVbRatioEnv = Environment.GetEnvironmentVariable("MIN_SAT_PER_VB_RATIO"); + MIN_SAT_PER_VB_RATIO = minSatPerVbRatioEnv!= null ? decimal.Parse(minSatPerVbRatioEnv) : MIN_SAT_PER_VB_RATIO; + + var maxSatPerVbRatioEnv = Environment.GetEnvironmentVariable("MAX_SAT_PER_VB_RATIO"); + MAX_SAT_PER_VB_RATIO = maxSatPerVbRatioEnv!= null ? decimal.Parse(maxSatPerVbRatioEnv) : MAX_SAT_PER_VB_RATIO; + } } diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index b5e12173..28e75aeb 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -194,9 +194,9 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig) var outputVirtualSize = combinedPSBT.GetGlobalTransaction().GetVirtualSize() + 43; - var feeRateResult = await LightningHelper.GetFeeRateResult(network, _nbXplorerService); + var initialFeeRate = await LightningHelper.GetFeeRateResult(network, _nbXplorerService); - var totalFees = new Money(outputVirtualSize * feeRateResult.FeeRate.SatoshiPerByte, MoneyUnit.Satoshi); + var totalFees = new Money(outputVirtualSize * initialFeeRate.FeeRate.SatoshiPerByte, MoneyUnit.Satoshi); long fundingAmount = channelOperationRequest.Changeless ? channelOperationRequest.SatsAmount - totalFees : channelOperationRequest.SatsAmount; //We prepare the request (shim) with the base PSBT we had presigned with the UTXOs to fund the channel @@ -404,7 +404,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) { var totalIn = fundedPSBT.Inputs.Sum(i => i.GetTxOut()?.Value); //We manually fix the change (it was wrong from the Base template due to nbitcoin requiring a change on a PSBT) - var totalChangefulFees = new Money(vsize * feeRateResult.FeeRate.SatoshiPerByte, MoneyUnit.Satoshi); + var totalChangefulFees = new Money(vsize * initialFeeRate.FeeRate.SatoshiPerByte, MoneyUnit.Satoshi); var changeOutput = channelfundingTx.Outputs.SingleOrDefault(o => o.Value != channelOperationRequest.SatsAmount) ?? channelfundingTx.Outputs.First(); changeOutput.Value = totalIn - totalOut - totalChangefulFees; @@ -473,7 +473,38 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest) finalizedPSBT.AssertSanity(); channelfundingTx = finalizedPSBT.ExtractTransaction(); - + + //We check the feerate of the finalized PSBT by checking a minimum and maximum allowed and also a fee-level max check in ratio + var feerate = new FeeRate(finalizedPSBT.GetFee(), channelfundingTx.GetVirtualSize()); + + var minFeeRate = Constants.MIN_SAT_PER_VB_RATIO * initialFeeRate.FeeRate.SatoshiPerByte; + + var maxFeeRate = Constants.MAX_SAT_PER_VB_RATIO * initialFeeRate.FeeRate.SatoshiPerByte; + + if (feerate.SatoshiPerByte < minFeeRate) + { + _logger.LogError("Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb} is lower than the minimum allowed: {MinSatPerVb}", channelOperationRequest.Id, feerate.SatoshiPerByte, minFeeRate); + throw new Exception("The finalized PSBT sat/vb is lower than the minimum allowed"); + } + + if (feerate.SatoshiPerByte > maxFeeRate) + { + _logger.LogError("Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb} is higher than the maximum allowed: {MaxSatPerVb}", channelOperationRequest.Id, feerate.SatoshiPerByte, maxFeeRate); + throw new Exception("The finalized PSBT sat/vb is higher than the maximum allowed"); + } + + //if the fee is too high, we throw an exception + var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long) x.GetCoin()?.Amount); + if (finalizedPSBT.GetFee().Satoshi >= + finalizedTotalIn * Constants.MAX_TX_FEE_RATIO) + { + _logger.LogError("Channel operation request id: {RequestId} finalized PSBT fee: {Fee} is higher than the maximum allowed: {MaxFee} sats", channelOperationRequest.Id, finalizedPSBT.GetFee().Satoshi, finalizedTotalIn * Constants.MAX_TX_FEE_RATIO); + throw new Exception("The finalized PSBT fee is higher than the maximum allowed"); + } + + + _logger.LogInformation("Channel operation request id: {RequestId} finalized PSBT sat/vb: {SatPerVb}", channelOperationRequest.Id, feerate.SatoshiPerByte); + //Just a check of the tx based on the finalizedPSBT var checkTx = channelfundingTx.Check();