Skip to content

Commit

Permalink
Check upfront shutdown script opt (#235)
Browse files Browse the repository at this point in the history
* Check upfront shutdown script feature

* Check IsKnown flag

* Changed GetCloseAddress signature

* Added unit testing for closing address

---------

Co-authored-by: Rodrigo Sanchez <rodrigo@clovrlabs.com>
  • Loading branch information
RodriFS and Rodrigo Sanchez authored Jul 20, 2023
1 parent 6bfdd36 commit e0c5a98
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 56 deletions.
116 changes: 74 additions & 42 deletions src/Services/LightningService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)

var network = CurrentNetworkHelper.GetCurrentNetwork();

var closeAddress = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger);

_logger.LogInformation("Channel open request for request id: {RequestId} from node: {SourceNodeName} to node: {DestinationNodeName}",
channelOperationRequest.Id,
Expand All @@ -193,54 +192,24 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
try
{
var humanSignaturesCount = channelOperationRequest.ChannelOperationRequestPsbts.Count(
x => channelOperationRequest.Wallet != null &&
!x.IsFinalisedPSBT &&
!x.IsInternalWalletPSBT &&
x => channelOperationRequest.Wallet != null &&
!x.IsFinalisedPSBT &&
!x.IsInternalWalletPSBT &&
!x.IsTemplatePSBT);

//If it is a hot wallet, we dont check the number of (human) signatures
if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN -1)
if (channelOperationRequest.Wallet != null && !channelOperationRequest.Wallet.IsHotWallet && channelOperationRequest.Wallet != null && humanSignaturesCount != channelOperationRequest.Wallet.MofN - 1)
{
_logger.LogError("The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", channelOperationRequest.Wallet.MofN-1, humanSignaturesCount);
_logger.LogError("The number of human signatures does not match the number of signatures required for this wallet, expected {MofN} but got {HumanSignaturesCount}", channelOperationRequest.Wallet.MofN - 1, humanSignaturesCount);
throw new InvalidOperationException("The number of human signatures does not match the number of signatures required for this wallet");
}
if (!combinedPSBT.TryGetVirtualSize(out var estimatedVsize))
{
_logger.LogError("Could not estimate virtual size of the PSBT");
throw new InvalidOperationException("Could not estimate virtual size of the PSBT");
}

if(channelOperationRequest.Changeless && combinedPSBT.Outputs.Any())
if (channelOperationRequest.Changeless && combinedPSBT.Outputs.Any())
{
_logger.LogError("Changeless channel operation request cannot have outputs at this stage");
throw new InvalidOperationException("Changeless channel operation request cannot have outputs at this stage");
}

var changelessVSize = channelOperationRequest.Changeless ? 43 : 0; // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig)
var outputVirtualSize = estimatedVsize + changelessVSize; // We add the change output if needed
var initialFeeRate = channelOperationRequest.FeeRate ?? (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate.SatoshiPerByte;;

var totalFees = new Money(outputVirtualSize * initialFeeRate, 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
var openChannelRequest = new OpenChannelRequest
{
FundingShim = new FundingShim
{
PsbtShim = new PsbtShim
{
BasePsbt = ByteString.FromBase64(combinedPSBT.ToBase64()),
NoPublish = false,
PendingChanId = ByteString.CopyFrom(pendingChannelId)
}
},
LocalFundingAmount = fundingAmount,
CloseAddress = closeAddress.Address.ToString(),
Private = channelOperationRequest.IsChannelPrivate,
NodePubkey = ByteString.CopyFrom(Convert.FromHexString(destination.PubKey)),
};

//Prior to opening the channel, we add the remote node as a peer
var remoteNodeInfo = await GetNodeInfo(channelOperationRequest.DestNode?.PubKey);
if (remoteNodeInfo == null)
Expand All @@ -250,8 +219,15 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
throw new InvalidOperationException();
}

var initialFeeRate = channelOperationRequest.FeeRate ?? (await LightningHelper.GetFeeRateResult(network, _nbXplorerService)).FeeRate.SatoshiPerByte;
;

var fundingAmount = GetFundingAmount(channelOperationRequest, combinedPSBT, initialFeeRate);

var openChannelRequest = await CreateOpenChannelRequest(channelOperationRequest, combinedPSBT, remoteNodeInfo, fundingAmount, pendingChannelId, derivationStrategyBase);

//For now, we only rely on pure tcp IPV4 connections
var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp").Addr;
var addr = remoteNodeInfo.Addresses.FirstOrDefault(x => x.Network == "tcp")?.Addr;

if (addr == null)
{
Expand Down Expand Up @@ -281,6 +257,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
{
throw new PeerNotOnlineException($"$peer {destination.PubKey} is not online");
}

if (!e.Message.Contains("already connected to peer"))
{
throw;
Expand Down Expand Up @@ -346,6 +323,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
{
channelOperationRequest.StatusLogs.Add(ChannelStatusLog.Info($"Channel opened successfully 🎉"));
}

_channelOperationRequestRepository.Update(channelOperationRequest);

var fundingTx = LightningHelper.DecodeTxId(response.ChanOpen.ChannelPoint.FundingTxidBytes);
Expand All @@ -368,7 +346,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
CreationDatetime = DateTimeOffset.Now,
FundingTx = fundingTx,
FundingTxOutputIndex = response.ChanOpen.ChannelPoint.OutputIndex,
BtcCloseAddress = closeAddress?.Address.ToString(),
BtcCloseAddress = openChannelRequest.CloseAddress,
SatsAmount = channelOperationRequest.SatsAmount,
UpdateDatetime = DateTimeOffset.Now,
Status = Channel.ChannelStatus.Open,
Expand Down Expand Up @@ -526,7 +504,7 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
}

//if the fee is too high, we throw an exception
var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long) x.GetCoin()?.Amount);
var finalizedTotalIn = finalizedPSBT.Inputs.Sum(x => (long)x.GetCoin()?.Amount);
if (finalizedPSBT.GetFee().Satoshi >=
finalizedTotalIn * Constants.MAX_TX_FEE_RATIO)
{
Expand Down Expand Up @@ -626,10 +604,12 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
// TODO: Make exception message pretty
throw new RemoteCanceledFundingException(e.Message);
}

if (e.Message.Contains("is not online"))
{
throw new PeerNotOnlineException($"$peer {destination.PubKey} is not online");
}

throw;
}
}
Expand All @@ -655,6 +635,58 @@ public async Task OpenChannel(ChannelOperationRequest channelOperationRequest)
return new Lightning.LightningClient(grpcChannel).Wrap();
};

public long GetFundingAmount(ChannelOperationRequest channelOperationRequest, PSBT combinedPSBT, decimal initialFeeRate)
{
if (!combinedPSBT.TryGetVirtualSize(out var estimatedVsize))
{
_logger.LogError("Could not estimate virtual size of the PSBT");
throw new InvalidOperationException("Could not estimate virtual size of the PSBT");
}

var changelessVSize = channelOperationRequest.Changeless ? 43 : 0; // 8 value + 1 script pub key size + 34 script pub key hash (Segwit output 2-0f-2 multisig)
var outputVirtualSize = estimatedVsize + changelessVSize; // We add the change output if needed

var totalFees = new Money(outputVirtualSize * initialFeeRate, MoneyUnit.Satoshi);
return channelOperationRequest.Changeless ? channelOperationRequest.SatsAmount - totalFees : channelOperationRequest.SatsAmount;
}

public async Task<OpenChannelRequest> CreateOpenChannelRequest(ChannelOperationRequest channelOperationRequest, PSBT? combinedPSBT, LightningNode? remoteNodeInfo, long fundingAmount, byte[] pendingChannelId, DerivationStrategyBase? derivationStrategyBase)
{
if (combinedPSBT == null) throw new ArgumentNullException(nameof(combinedPSBT));
if (remoteNodeInfo == null) throw new ArgumentNullException(nameof(remoteNodeInfo));
if (derivationStrategyBase == null) throw new ArgumentNullException(nameof(derivationStrategyBase));

//We prepare the request (shim) with the base PSBT we had presigned with the UTXOs to fund the channel
var openChannelRequest = new OpenChannelRequest
{
FundingShim = new FundingShim
{
PsbtShim = new PsbtShim
{
BasePsbt = ByteString.FromBase64(combinedPSBT.ToBase64()),
NoPublish = false,
PendingChanId = ByteString.CopyFrom(pendingChannelId)
}
},
LocalFundingAmount = fundingAmount,
Private = channelOperationRequest.IsChannelPrivate,
NodePubkey = ByteString.CopyFrom(Convert.FromHexString(remoteNodeInfo.PubKey)),
};

// Check features to see if we need or is allowed to add a close address
var upfrontShutdownScriptOpt = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptOpt);
var upfrontShutdownScriptReq = remoteNodeInfo.Features.ContainsKey((uint)FeatureBit.UpfrontShutdownScriptReq);
if (upfrontShutdownScriptOpt && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptOpt] is { IsKnown: true } ||
upfrontShutdownScriptReq && remoteNodeInfo.Features[(uint)FeatureBit.UpfrontShutdownScriptReq] is { IsKnown: true })
{
var address = await GetCloseAddress(channelOperationRequest, derivationStrategyBase, _nbXplorerService, _logger);
openChannelRequest.CloseAddress = address.Address.ToString();
;
}

return openChannelRequest;
}

public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationRequest, ILogger? _logger = null)
{
//PSBT Combine
Expand All @@ -672,7 +704,7 @@ public static PSBT GetCombinedPsbt(ChannelOperationRequest channelOperationReque
throw new ArgumentException(invalidPsbtNullToBeUsedForTheRequest, nameof(combinedPSBT));
}

public static async Task<KeyPathInformation?> GetCloseAddress(ChannelOperationRequest channelOperationRequest,
public static async Task<KeyPathInformation> GetCloseAddress(ChannelOperationRequest channelOperationRequest,
DerivationStrategyBase derivationStrategyBase, INBXplorerService nbXplorerService, ILogger? _logger = null)
{
var closeAddress = await
Expand Down
Loading

0 comments on commit e0c5a98

Please sign in to comment.